Compare commits
2 Commits
master
...
feat/test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0beb11bade | ||
|
|
84c459dd77 |
@@ -15,6 +15,7 @@
|
|||||||
:session-id="sessionId || null"
|
:session-id="sessionId || null"
|
||||||
:platform-id="sessionPlatformId"
|
:platform-id="sessionPlatformId"
|
||||||
:is-group="sessionIsGroup"
|
:is-group="sessionIsGroup"
|
||||||
|
:initial-config-id="props.configId"
|
||||||
@config-changed="handleConfigChange"
|
@config-changed="handleConfigChange"
|
||||||
/>
|
/>
|
||||||
<ProviderModelSelector v-if="showProviderSelector" ref="providerModelSelectorRef" />
|
<ProviderModelSelector v-if="showProviderSelector" ref="providerModelSelectorRef" />
|
||||||
@@ -79,11 +80,13 @@ interface Props {
|
|||||||
isRecording: boolean;
|
isRecording: boolean;
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
currentSession?: Session | null;
|
currentSession?: Session | null;
|
||||||
|
configId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
sessionId: null,
|
sessionId: null,
|
||||||
currentSession: null
|
currentSession: null,
|
||||||
|
configId: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -90,10 +90,12 @@ const props = withDefaults(defineProps<{
|
|||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
platformId?: string;
|
platformId?: string;
|
||||||
isGroup?: boolean;
|
isGroup?: boolean;
|
||||||
|
initialConfigId?: string | null;
|
||||||
}>(), {
|
}>(), {
|
||||||
sessionId: null,
|
sessionId: null,
|
||||||
platformId: 'webchat',
|
platformId: 'webchat',
|
||||||
isGroup: false
|
isGroup: false,
|
||||||
|
initialConfigId: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
||||||
@@ -291,7 +293,7 @@ watch(
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchConfigList();
|
await fetchConfigList();
|
||||||
const stored = localStorage.getItem(STORAGE_KEY) || 'default';
|
const stored = props.initialConfigId || localStorage.getItem(STORAGE_KEY) || 'default';
|
||||||
selectedConfigId.value = stored;
|
selectedConfigId.value = stored;
|
||||||
await setSelection(stored);
|
await setSelection(stored);
|
||||||
await syncSelectionForSession();
|
await syncSelectionForSession();
|
||||||
|
|||||||
319
dashboard/src/components/chat/StandaloneChat.vue
Normal file
319
dashboard/src/components/chat/StandaloneChat.vue
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="standalone-chat-card" elevation="0" rounded="0">
|
||||||
|
<v-card-text class="standalone-chat-container">
|
||||||
|
<div class="chat-layout">
|
||||||
|
<!-- 聊天内容区域 -->
|
||||||
|
<div class="chat-content-panel">
|
||||||
|
<MessageList v-if="messages && messages.length > 0" :messages="messages" :isDark="isDark"
|
||||||
|
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
|
||||||
|
ref="messageList" />
|
||||||
|
<div class="welcome-container fade-in" v-else>
|
||||||
|
<div class="welcome-title">
|
||||||
|
<span>Hello, I'm</span>
|
||||||
|
<span class="bot-name">AstrBot ⭐</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-caption text-medium-emphasis mt-2">
|
||||||
|
测试配置: {{ configId || 'default' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<ChatInput
|
||||||
|
v-model:prompt="prompt"
|
||||||
|
:stagedImagesUrl="stagedImagesUrl"
|
||||||
|
:stagedAudioUrl="stagedAudioUrl"
|
||||||
|
:disabled="isStreaming || isConvRunning"
|
||||||
|
:enableStreaming="enableStreaming"
|
||||||
|
:isRecording="isRecording"
|
||||||
|
:session-id="currSessionId || null"
|
||||||
|
:current-session="getCurrentSession"
|
||||||
|
:config-id="configId"
|
||||||
|
@send="handleSendMessage"
|
||||||
|
@toggleStreaming="toggleStreaming"
|
||||||
|
@removeImage="removeImage"
|
||||||
|
@removeAudio="removeAudio"
|
||||||
|
@startRecording="handleStartRecording"
|
||||||
|
@stopRecording="handleStopRecording"
|
||||||
|
@pasteImage="handlePaste"
|
||||||
|
@fileSelect="handleFileSelect"
|
||||||
|
ref="chatInputRef"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 图片预览对话框 -->
|
||||||
|
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||||
|
<v-card class="image-preview-card" elevation="8">
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||||
|
<span>{{ t('core.common.imagePreview') }}</span>
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="text-center pa-4">
|
||||||
|
<img :src="previewImageUrl" class="preview-image-large" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useCustomizerStore } from '@/stores/customizer';
|
||||||
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
import MessageList from '@/components/chat/MessageList.vue';
|
||||||
|
import ChatInput from '@/components/chat/ChatInput.vue';
|
||||||
|
import { useMessages } from '@/composables/useMessages';
|
||||||
|
import { useMediaHandling } from '@/composables/useMediaHandling';
|
||||||
|
import { useRecording } from '@/composables/useRecording';
|
||||||
|
import { useToast } from '@/utils/toast';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
configId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
configId: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { error: showError } = useToast();
|
||||||
|
|
||||||
|
// UI 状态
|
||||||
|
const imagePreviewDialog = ref(false);
|
||||||
|
const previewImageUrl = ref('');
|
||||||
|
|
||||||
|
// 会话管理(不使用 useSessions 避免路由跳转)
|
||||||
|
const currSessionId = ref('');
|
||||||
|
const getCurrentSession = computed(() => null); // 独立测试模式不需要会话信息
|
||||||
|
|
||||||
|
async function newSession() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/chat/new_session');
|
||||||
|
const sessionId = response.data.data.session_id;
|
||||||
|
currSessionId.value = sessionId;
|
||||||
|
return sessionId;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionTitle(sessionId: string, title: string) {
|
||||||
|
// 独立模式不需要更新会话标题
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessions() {
|
||||||
|
// 独立模式不需要加载会话列表
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
stagedImagesName,
|
||||||
|
stagedImagesUrl,
|
||||||
|
stagedAudioUrl,
|
||||||
|
getMediaFile,
|
||||||
|
processAndUploadImage,
|
||||||
|
handlePaste,
|
||||||
|
removeImage,
|
||||||
|
removeAudio,
|
||||||
|
clearStaged,
|
||||||
|
cleanupMediaCache
|
||||||
|
} = useMediaHandling();
|
||||||
|
|
||||||
|
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
|
||||||
|
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isStreaming,
|
||||||
|
isConvRunning,
|
||||||
|
enableStreaming,
|
||||||
|
getSessionMessages: getSessionMsg,
|
||||||
|
sendMessage: sendMsg,
|
||||||
|
toggleStreaming
|
||||||
|
} = useMessages(currSessionId, getMediaFile, updateSessionTitle, getSessions);
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const messageList = ref<InstanceType<typeof MessageList> | null>(null);
|
||||||
|
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
|
||||||
|
|
||||||
|
// 输入状态
|
||||||
|
const prompt = ref('');
|
||||||
|
|
||||||
|
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
|
||||||
|
|
||||||
|
function openImagePreview(imageUrl: string) {
|
||||||
|
previewImageUrl.value = imageUrl;
|
||||||
|
imagePreviewDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStartRecording() {
|
||||||
|
await startRec();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStopRecording() {
|
||||||
|
const audioFilename = await stopRec();
|
||||||
|
stagedAudioUrl.value = audioFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileSelect(files: FileList) {
|
||||||
|
for (const file of files) {
|
||||||
|
await processAndUploadImage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSendMessage() {
|
||||||
|
if (!prompt.value.trim() && stagedImagesName.value.length === 0 && !stagedAudioUrl.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!currSessionId.value) {
|
||||||
|
await newSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptToSend = prompt.value.trim();
|
||||||
|
const imageNamesToSend = [...stagedImagesName.value];
|
||||||
|
const audioNameToSend = stagedAudioUrl.value;
|
||||||
|
|
||||||
|
// 清空输入和附件
|
||||||
|
prompt.value = '';
|
||||||
|
clearStaged();
|
||||||
|
|
||||||
|
// 获取选择的提供商和模型
|
||||||
|
const selection = chatInputRef.value?.getCurrentSelection();
|
||||||
|
const selectedProviderId = selection?.providerId || '';
|
||||||
|
const selectedModelName = selection?.modelName || '';
|
||||||
|
|
||||||
|
await sendMsg(
|
||||||
|
promptToSend,
|
||||||
|
imageNamesToSend,
|
||||||
|
audioNameToSend,
|
||||||
|
selectedProviderId,
|
||||||
|
selectedModelName
|
||||||
|
);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
nextTick(() => {
|
||||||
|
messageList.value?.scrollToBottom();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to send message:', err);
|
||||||
|
showError(t('features.chat.errors.sendMessageFailed'));
|
||||||
|
// 恢复输入内容,让用户可以重试
|
||||||
|
// 注意:附件已经上传到服务器,所以不恢复附件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 独立模式在挂载时创建新会话
|
||||||
|
try {
|
||||||
|
await newSession();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create initial session:', err);
|
||||||
|
showError(t('features.chat.errors.createSessionFailed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanupMediaCache();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 基础动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.standalone-chat-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standalone-chat-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-layout {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content-panel {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
padding-left: 16px;
|
||||||
|
border-bottom: 1px solid var(--v-theme-border);
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header-info h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-name {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--v-theme-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-large {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -85,5 +85,9 @@
|
|||||||
"reconnected": "Chat connection re-established",
|
"reconnected": "Chat connection re-established",
|
||||||
"failed": "Connection failed, please refresh the page"
|
"failed": "Connection failed, please refresh the page"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"sendMessageFailed": "Failed to send message, please try again",
|
||||||
|
"createSessionFailed": "Failed to create session, please refresh the page"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,5 +85,9 @@
|
|||||||
"reconnected": "聊天连接已重新建立",
|
"reconnected": "聊天连接已重新建立",
|
||||||
"failed": "连接失败,请刷新页面重试"
|
"failed": "连接失败,请刷新页面重试"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"sendMessageFailed": "发送消息失败,请重试",
|
||||||
|
"createSessionFailed": "创建会话失败,请刷新页面重试"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,15 @@
|
|||||||
@click="configToString(); codeEditorDialog = true">
|
@click="configToString(); codeEditorDialog = true">
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<v-tooltip text="测试当前配置" location="left" v-if="!isSystemConfig">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-chat-processing" size="x-large"
|
||||||
|
style="position: fixed; right: 52px; bottom: 196px;" color="secondary"
|
||||||
|
@click="openTestChat">
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</v-slide-y-transition>
|
</v-slide-y-transition>
|
||||||
|
|
||||||
@@ -135,6 +144,34 @@
|
|||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||||
|
|
||||||
|
<!-- 测试聊天抽屉 -->
|
||||||
|
<v-overlay
|
||||||
|
v-model="testChatDrawer"
|
||||||
|
class="test-chat-overlay"
|
||||||
|
location="right"
|
||||||
|
transition="slide-x-reverse-transition"
|
||||||
|
:scrim="true"
|
||||||
|
@click:outside="closeTestChat"
|
||||||
|
>
|
||||||
|
<v-card class="test-chat-card" elevation="12">
|
||||||
|
<div class="test-chat-header">
|
||||||
|
<div>
|
||||||
|
<span class="text-h6">测试配置</span>
|
||||||
|
<div v-if="selectedConfigInfo.name" class="text-caption text-grey">
|
||||||
|
{{ selectedConfigInfo.name }} ({{ testConfigId }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-btn icon variant="text" @click="closeTestChat">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<div class="test-chat-content">
|
||||||
|
<StandaloneChat v-if="testChatDrawer" :configId="testConfigId" />
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-overlay>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -142,6 +179,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
import AstrBotCoreConfigWrapper from '@/components/config/AstrBotCoreConfigWrapper.vue';
|
||||||
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
|
||||||
|
import StandaloneChat from '@/components/chat/StandaloneChat.vue';
|
||||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
|
|
||||||
@@ -150,7 +188,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AstrBotCoreConfigWrapper,
|
AstrBotCoreConfigWrapper,
|
||||||
VueMonacoEditor,
|
VueMonacoEditor,
|
||||||
WaitingForRestart
|
WaitingForRestart,
|
||||||
|
StandaloneChat
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
initialConfigId: {
|
initialConfigId: {
|
||||||
@@ -238,6 +277,10 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
editingConfigId: null,
|
editingConfigId: null,
|
||||||
|
|
||||||
|
// 测试聊天
|
||||||
|
testChatDrawer: false,
|
||||||
|
testConfigId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -506,6 +549,20 @@ export default {
|
|||||||
this.getConfigInfoList("default");
|
this.getConfigInfoList("default");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
openTestChat() {
|
||||||
|
if (!this.selectedConfigID) {
|
||||||
|
this.save_message = "请先选择一个配置文件";
|
||||||
|
this.save_message_snack = true;
|
||||||
|
this.save_message_success = "warning";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.testConfigId = this.selectedConfigID;
|
||||||
|
this.testChatDrawer = true;
|
||||||
|
},
|
||||||
|
closeTestChat() {
|
||||||
|
this.testChatDrawer = false;
|
||||||
|
this.testConfigId = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -565,4 +622,32 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 测试聊天抽屉样式 */
|
||||||
|
.test-chat-overlay {
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-chat-card {
|
||||||
|
width: clamp(320px, 50vw, 720px);
|
||||||
|
height: calc(100vh - 32px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-chat-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-chat-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user