1096 lines
26 KiB
Plaintext
1096 lines
26 KiB
Plaintext
<!-- AI聊天助手页面 - UTSJSONObject 优化版本 -->
|
||
<template>
|
||
<view class="chat-page">
|
||
<!-- 聊天头部 -->
|
||
<view class="chat-header">
|
||
<view class="header-content">
|
||
<view class="back-btn" @click="goBack">
|
||
<text class="back-icon">←</text>
|
||
</view>
|
||
<view class="header-info">
|
||
<text class="header-title">{{ $t('mt.title.chat') }}</text>
|
||
<text class="header-subtitle">{{ isTyping ? $t('mt.chat.typing') : $t('mt.chat.online') }}</text>
|
||
</view>
|
||
<view class="header-actions">
|
||
<view class="action-btn" @click="showSessionModal">
|
||
<text class="action-icon">📝</text>
|
||
</view>
|
||
<view class="action-btn" @click="clearChat">
|
||
<text class="action-icon">🗑</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 聊天消息列表 -->
|
||
<scroll-view direction="vertical" class="chat-messages" :scroll-y="true" :scroll-into-view="scrollIntoView"
|
||
:enable-back-to-top="false">
|
||
|
||
<!-- 欢迎消息 -->
|
||
<view class="welcome-message" v-if="messagesList.length === 0">
|
||
<view class="welcome-content">
|
||
<text class="welcome-icon">🤖</text>
|
||
<text class="welcome-title">{{ $t('mt.title.chat') }}</text>
|
||
<text class="welcome-text">{{ $t('mt.chat.welcome') }}</text>
|
||
<view class="quick-actions">
|
||
<view class="quick-action" @click="askQuickQuestion($t('mt.chat.quick.hotNewsQ'))">
|
||
<text class="quick-text">{{ $t('mt.chat.quick.hotNews') }}</text>
|
||
</view>
|
||
<view class="quick-action" @click="askQuickQuestion($t('mt.chat.quick.techTrendQ'))">
|
||
<text class="quick-text">{{ $t('mt.chat.quick.techTrend') }}</text>
|
||
</view>
|
||
<view class="quick-action" @click="askQuickQuestion($t('mt.chat.quick.todaySummaryQ'))">
|
||
<text class="quick-text">{{ $t('mt.chat.quick.todaySummary') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 消息列表 -->
|
||
<view class="messages-list">
|
||
<view v-for="message in messagesList" :key="message.id" :id="`msg-${message.id}`" class="message-item"
|
||
:class="{ 'message-user': message.message_type === 'user', 'message-assistant': message.message_type === 'assistant' }">
|
||
|
||
<!-- 用户消息 -->
|
||
<view class="message-content" v-if="message.message_type === 'user'">
|
||
<view class="message-bubble user-bubble">
|
||
<text class="message-text">{{ message.content }}</text>
|
||
<text class="message-time">{{ formatRelativeTimeKey(message.created_at) }}</text>
|
||
</view>
|
||
<view class="message-avatar user-avatar">
|
||
<text class="avatar-text">我</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI助手消息 -->
|
||
<view class="message-content" v-if="message.message_type === 'assistant'">
|
||
<view class="message-avatar assistant-avatar">
|
||
<text class="avatar-text">AI</text>
|
||
</view>
|
||
<view class="message-bubble assistant-bubble">
|
||
<text class="message-text" mode="native" :nodes="message.content">{{ message.content }}</text>
|
||
<view class="message-actions">
|
||
<view class="action-item" @click="copyMessage(message)">
|
||
<text class="action-icon">📋</text>
|
||
</view>
|
||
<view class="action-item" @click="likeMessage(message)">
|
||
<text class="action-icon">👍</text>
|
||
</view>
|
||
<view class="action-item" @click="dislikeMessage(message)">
|
||
<text class="action-icon">👎</text>
|
||
</view>
|
||
</view>
|
||
<text class="message-time">{{ formatRelativeTimeKey(message.created_at) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 打字状态 -->
|
||
<view class="typing-indicator" v-if="isTyping">
|
||
<view class="message-avatar assistant-avatar">
|
||
<text class="avatar-text">AI</text>
|
||
</view>
|
||
<view class="typing-bubble">
|
||
<view class="typing-dots">
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
<view class="dot"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 输入区域 -->
|
||
<view class="chat-input-section">
|
||
<view class="input-container">
|
||
<view class="input-wrapper">
|
||
<input class="chat-input" :value="inputMessage" @input="onInputChange" @confirm="sendMessage"
|
||
:placeholder="$t('mt.chat.inputPlaceholder')" confirm-type="send" :disabled="isTyping" />
|
||
<view class="input-actions">
|
||
<view class="action-btn" @click="showQuickActions" v-if="inputMessage === ''">
|
||
<text class="action-icon">⚡</text>
|
||
</view>
|
||
<view class="action-btn" @click="clearInput" v-if="inputMessage !== ''">
|
||
<text class="action-icon">✕</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="send-btn" :class="{ disabled: inputMessage === '' || isTyping }" @click="sendMessage">
|
||
<text class="send-icon">{{ isTyping ? $t('mt.chat.sending') : $t('mt.chat.send') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快捷操作弹窗 -->
|
||
<view class="quick-modal" v-if="showQuickModal" @click="hideQuickActions">
|
||
<view class="quick-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.chat.quick.title') }}</text>
|
||
<view class="close-btn" @click="hideQuickActions">
|
||
<text class="close-text">✕</text>
|
||
</view>
|
||
</view>
|
||
<view class="quick-list">
|
||
<view class="quick-category">
|
||
<text class="category-title">{{ $t('mt.chat.quick.recommend') }}</text>
|
||
<view class="category-items">
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.techNewsQ'))">
|
||
<text class="quick-icon">🔬</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.techNews') }}</text>
|
||
</view>
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.economyNewsQ'))">
|
||
<text class="quick-icon">💰</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.economyNews') }}</text>
|
||
</view>
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.sportNewsQ'))">
|
||
<text class="quick-icon">⚽</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.sportNews') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="quick-category">
|
||
<text class="category-title">{{ $t('mt.chat.quick.analysis') }}</text>
|
||
<view class="category-items">
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.hotTopicQ'))">
|
||
<text class="quick-icon">📊</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.hotTopic') }}</text>
|
||
</view>
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.newsSummaryQ'))">
|
||
<text class="quick-icon">📈</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.newsSummary') }}</text>
|
||
</view>
|
||
<view class="quick-item" @click="askQuickQuestion($t('mt.chat.quick.conceptQ'))">
|
||
<text class="quick-icon">💡</text>
|
||
<text class="quick-label">{{ $t('mt.chat.quick.concept') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 会话管理弹窗 -->
|
||
<view class="session-modal" v-if="showSessionPanel" @click="hideSessionModal">
|
||
<view class="session-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.chat.session.title') }}</text>
|
||
<view class="close-btn" @click="hideSessionModal">
|
||
<text class="close-text">✕</text>
|
||
</view>
|
||
</view>
|
||
<view class="session-list">
|
||
<view v-for="session in sessionsList" :key="session.id" class="session-item"
|
||
:class="{ active: currentSessionId === session.id }" @click="switchSession(session)">
|
||
<view class="session-info">
|
||
<text class="session-name">{{ session.session_name }}</text>
|
||
<text class="session-time">{{ formatRelativeTimeKey(session.last_message_at) }}</text>
|
||
</view>
|
||
<view class="session-stats">
|
||
<text
|
||
class="session-count">{{ session.total_messages }}{{ $t('mt.chat.session.msgCount') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="session-actions">
|
||
<view class="action-btn secondary" @click="createNewSession">
|
||
<text class="action-text">{{ $t('mt.chat.session.new') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
// 消息操作
|
||
import { setClipboardData, getClipboardData, SetClipboardDataOption, GetClipboardDataOption, GetClipboardDataSuccessCallbackOption } from '@/uni_modules/lime-clipboard'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
import { requestCanvasCompletion } from '@/components/supadb/rag.uts'
|
||
import { RagReq } from '@/uni_modules/rag-req/rag-req.uts'
|
||
import { RagSessionData, AgentSessionListOptions } from '@/uni_modules/rag-req/interface.uts'
|
||
|
||
import { RAG_API_KEY, RAG_BASE_URL, RAG_AGENT_ID } from '@/ak/config'
|
||
import rag from '@/components/supadb/raginstance.uts'
|
||
|
||
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
|
||
import type {
|
||
ChatMessageData,
|
||
ChatSessionData,
|
||
ChatState
|
||
} from './types.uts'
|
||
import {
|
||
formatRelativeTimeKey
|
||
} from './types.uts'
|
||
|
||
// 聊天状态
|
||
const chatState = ref<ChatState>({
|
||
isTyping: false,
|
||
currentSession: null,
|
||
messageCount: 0
|
||
})
|
||
|
||
// 页面状态
|
||
const inputMessage = ref<string>('')
|
||
const scrollIntoView = ref<string>('')
|
||
const showQuickModal = ref<boolean>(false)
|
||
const showSessionPanel = ref<boolean>(false)
|
||
|
||
// 数据列表
|
||
const messagesList = ref<Array<ChatMessageData>>([])
|
||
const sessionsList = ref<Array<ChatSessionData>>([])
|
||
|
||
// 当前会话
|
||
const currentSessionId = ref<string>('')
|
||
const contextContent = ref<string>('')
|
||
|
||
// 计算属性
|
||
const isTyping = computed(() : boolean => {
|
||
return chatState.value.isTyping
|
||
})
|
||
|
||
// 输入处理
|
||
const onInputChange = (event : InputEvent) => {
|
||
inputMessage.value = event.detail.value
|
||
}
|
||
|
||
const clearInput = () => {
|
||
inputMessage.value = ''
|
||
}
|
||
|
||
// 滚动到底部
|
||
const scrollToBottom = () => {
|
||
nextTick(() => {
|
||
if (messagesList.value.length > 0) {
|
||
const lastMessage = messagesList.value[messagesList.value.length - 1]
|
||
const messageId = lastMessage.id
|
||
scrollIntoView.value = `msg-${messageId}`
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 扩展:支持传入完整AI返回对象,自动提取 id/session_id/answer/message/data 字段
|
||
* @param aiData 可以是 string(仅内容),也可以是对象(含 id/session_id/answer/message/data)
|
||
*/
|
||
const addAssistantMessage = (aiData : any) => {
|
||
console.log(aiData)
|
||
let content = ''
|
||
let id = ''
|
||
let session_id = ''
|
||
let created_at = new Date().toISOString()
|
||
const obj = aiData as UTSJSONObject
|
||
content = obj.getString('answer') ?? obj.getString('message') ?? obj.getString('data') ?? ''
|
||
id = obj.getString('id') ?? ''
|
||
session_id = obj.getString('session_id') ?? ''
|
||
if (content == '') content = 'AI助手暂无回复内容。'
|
||
if (id == '') id = `assistant-${Date.now()}`
|
||
const assistantMessage : ChatMessageData = {
|
||
id,
|
||
session_id,
|
||
message_type: 'assistant',
|
||
content,
|
||
created_at
|
||
}
|
||
console.log(assistantMessage)
|
||
messagesList.value.push(assistantMessage)
|
||
chatState.value.messageCount += 1
|
||
scrollToBottom()
|
||
}
|
||
|
||
const sendToAI = async (message : string) => {
|
||
chatState.value.isTyping = true
|
||
try {
|
||
let sessionId = currentSessionId.value
|
||
console.log(sessionId)
|
||
// 如果没有 sessionId,先创建
|
||
if (sessionId == null || sessionId == '') {
|
||
const sessionRes = await rag.createAgentSession(RAG_AGENT_ID, null, null, false)
|
||
console.log(sessionRes)
|
||
// 兼容API返回的单层或双层data结构,避免UTS Android unresolved reference: data
|
||
let sessionData : UTSJSONObject | null = null
|
||
if (sessionRes.data != null) {
|
||
const sb = sessionRes.data as UTSJSONObject | null
|
||
sessionData = sb?.getJSON('data')
|
||
}
|
||
if (sessionData != null && sessionData.get('id') != null && sessionData.get('id') != '') {
|
||
sessionId = sessionData.get('id').toString()
|
||
currentSessionId.value = sessionId
|
||
// RagReq.setSessionId(sessionId) // 可选
|
||
} else {
|
||
throw new Error('会话创建失败')
|
||
}
|
||
}
|
||
// 发起聊天
|
||
const body = {
|
||
question: message,
|
||
stream: false,
|
||
session_id: sessionId
|
||
} as UTSJSONObject
|
||
const result = await rag.converseWithAgent(RAG_AGENT_ID, body)
|
||
console.log(result)
|
||
const res_data:UTSJSONObject = result.data
|
||
console.log(res_data)
|
||
|
||
const res = res_data.getJSON('data')
|
||
let aiResponse = ''
|
||
if (res != null && typeof res === 'object') {
|
||
console.log(res)
|
||
// UTSJSONObject 类型
|
||
const answer = (res as UTSJSONObject).getString('answer') ?? (res as UTSJSONObject).getString('message') ?? (res as UTSJSONObject).getString('data') ?? ''
|
||
console.log(answer)
|
||
aiResponse = answer != '' ? answer : 'AI助手暂无回复内容。'
|
||
addAssistantMessage(res)
|
||
} else {
|
||
aiResponse = 'AI助手暂无回复内容。'+ res_data.code
|
||
addAssistantMessage(aiResponse)
|
||
}
|
||
} catch (error) {
|
||
console.error('AI回复失败:', error)
|
||
addAssistantMessage('抱歉,我现在无法回复您的消息,请稍后再试。')
|
||
} finally {
|
||
chatState.value.isTyping = false
|
||
}
|
||
}
|
||
const showQuickActions = () => {
|
||
showQuickModal.value = true
|
||
}
|
||
|
||
const hideQuickActions = () => {
|
||
showQuickModal.value = false
|
||
}
|
||
|
||
|
||
|
||
const copyMessage = async (message : ChatMessageData) => {
|
||
const content = message.content
|
||
try {
|
||
setClipboardData({
|
||
data: content,
|
||
success(res) {
|
||
console.log('res', res.errMsg)
|
||
uni.showToast({
|
||
title: '已复制到剪贴板',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
} as SetClipboardDataOption)
|
||
|
||
|
||
} catch (error) {
|
||
console.error('复制失败:', error)
|
||
}
|
||
}
|
||
|
||
const likeMessage = (message : ChatMessageData) => {
|
||
uni.showToast({
|
||
title: '感谢您的反馈',
|
||
icon: 'success'
|
||
})
|
||
|
||
// 这里可以发送用户反馈到服务器
|
||
console.log('用户点赞消息:', message)
|
||
}
|
||
|
||
const dislikeMessage = (message : ChatMessageData) => {
|
||
uni.showToast({
|
||
title: '感谢您的反馈',
|
||
icon: 'success'
|
||
})
|
||
|
||
// 这里可以发送用户反馈到服务器
|
||
console.log('用户踩消息:', message)
|
||
}
|
||
|
||
|
||
|
||
const hideSessionModal = () => {
|
||
showSessionPanel.value = false
|
||
}
|
||
|
||
|
||
|
||
// ...existing code...
|
||
const createNewSession = () => {
|
||
const newSession : ChatSessionData = {
|
||
id: `session-${Date.now()}`,
|
||
user_id: '',
|
||
session_name: `新会话 ${sessionsList.value.length + 1}`,
|
||
language: '',
|
||
context: contextContent.value,
|
||
ai_model: '',
|
||
total_messages: 0,
|
||
total_tokens: 0,
|
||
cost_usd: 0,
|
||
last_message_at: new Date().toISOString(),
|
||
is_active: true,
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString(),
|
||
}
|
||
sessionsList.value.unshift(newSession)
|
||
currentSessionId.value = newSession.id
|
||
messagesList.value = []
|
||
chatState.value.messageCount = 0
|
||
hideSessionModal()
|
||
}
|
||
const addUserMessage = (content : string, session_id : string) => {
|
||
const userMessage : ChatMessageData = {
|
||
id: `user-${Date.now()}`,
|
||
session_id: session_id,
|
||
message_type: 'user',
|
||
content,
|
||
created_at: new Date().toISOString(),
|
||
}
|
||
messagesList.value.push(userMessage)
|
||
chatState.value.messageCount += 1
|
||
scrollToBottom()
|
||
}
|
||
const sendMessage = async () => {
|
||
const message = inputMessage.value.trim()
|
||
if (message === '' || isTyping.value) return
|
||
console.log(message)
|
||
addUserMessage(message, currentSessionId.value)
|
||
clearInput()
|
||
await sendToAI(message)
|
||
}
|
||
|
||
// ...existing code...
|
||
const loadSessions = async () => {
|
||
// 这里应该从服务器加载会话列表
|
||
try {
|
||
// 新API需要传递 agentId 和 options,需用强类型对象
|
||
const options : AgentSessionListOptions = {} as AgentSessionListOptions // 空对象强转类型,或按需补充字段
|
||
const res = await rag.getAgentSessionList(RAG_AGENT_ID, options)
|
||
let sessionArr : Array<RagSessionData> = []
|
||
if (res != null && res.data != null) {
|
||
if (Array.isArray(res.data)) {
|
||
sessionArr = res.data as RagSessionData[]
|
||
}
|
||
}
|
||
if (sessionArr.length > 0) {
|
||
sessionsList.value = sessionArr as Array<ChatSessionData>
|
||
currentSessionId.value = sessionsList.value[0].id
|
||
} else {
|
||
// 没有历史session,先用 rag.createSession 创建一个
|
||
const sessionRes = await rag.createSession()
|
||
let sessionId = ''
|
||
let sessionName = ''
|
||
if (sessionRes?.data != null) {
|
||
const data = sessionRes?.data
|
||
console.log(data)
|
||
if (typeof data === 'object') {
|
||
sessionId = data?.id ?? ''
|
||
sessionName = data?.session_name ?? ''
|
||
}
|
||
}
|
||
if (sessionId !== '') {
|
||
// 只用id,直接设置currentSessionId并保存到storage
|
||
currentSessionId.value = sessionId
|
||
try {
|
||
uni.setStorageSync('ragreq_session_id', sessionId)
|
||
} catch (e) { }
|
||
// 也可选填充sessionsList(如需UI展示)
|
||
const newSession : ChatSessionData = {
|
||
id: sessionId,
|
||
user_id: '',
|
||
session_name: sessionName ?? '新会话' + sessionId,
|
||
language: '',
|
||
context: '',
|
||
ai_model: '',
|
||
total_messages: 0,
|
||
total_tokens: 0,
|
||
cost_usd: 0,
|
||
last_message_at: new Date().toISOString(),
|
||
is_active: true,
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString(),
|
||
}
|
||
sessionsList.value = [newSession]
|
||
} else {
|
||
// 彻底失败,清空会话列表
|
||
sessionsList.value = []
|
||
currentSessionId.value = ''
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// 彻底失败,清空会话列表
|
||
sessionsList.value = []
|
||
currentSessionId.value = ''
|
||
}
|
||
}
|
||
|
||
|
||
const loadSessionMessages = (sessionId : string) => {
|
||
// 这里应该从服务器加载指定会话的消息
|
||
// 简化实现,清空消息列表
|
||
messagesList.value = []
|
||
chatState.value.messageCount = 0
|
||
}
|
||
// ...existing code...
|
||
const switchSession = (session : ChatSessionData) => {
|
||
const sessionId = session.id
|
||
if (sessionId === currentSessionId.value) {
|
||
hideSessionModal()
|
||
return
|
||
}
|
||
currentSessionId.value = sessionId
|
||
// @ts-ignore: loadSessionMessages may be hoisted below
|
||
loadSessionMessages(sessionId)
|
||
hideSessionModal()
|
||
}
|
||
// 清空聊天
|
||
const clearChat = () => {
|
||
uni.showModal({
|
||
title: '清空聊天',
|
||
content: '确定要清空当前会话的所有消息吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
messagesList.value = []
|
||
chatState.value.messageCount = 0
|
||
|
||
uni.showToast({
|
||
title: '聊天记录已清空',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 快捷操作
|
||
const askQuickQuestion = (question : string) => {
|
||
inputMessage.value = question
|
||
hideQuickActions()
|
||
sendMessage()
|
||
}
|
||
|
||
// 会话管理
|
||
const showSessionModal = () => {
|
||
showSessionPanel.value = true
|
||
loadSessions()
|
||
}
|
||
|
||
// 导航函数
|
||
const goBack = () => {
|
||
try {
|
||
uni.navigateBack({
|
||
delta: 1
|
||
})
|
||
} catch (error) {
|
||
console.error('返回异常:', error)
|
||
}
|
||
}
|
||
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 不能直接用 async/await,需用 then
|
||
loadSessions()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理工作
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.chat-page {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.chat-header {
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.header-content {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.back-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 20px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 18px;
|
||
color: #374151;
|
||
}
|
||
|
||
.header-info {
|
||
flex: 1;
|
||
margin-left: 16px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.header-subtitle {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.header-actions {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-left: 8px;
|
||
width: 40px;
|
||
height: 40px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 20px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 16px;
|
||
color: #374151;
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
padding: 16px;
|
||
}
|
||
|
||
.welcome-message {
|
||
align-items: center;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.welcome-content {
|
||
align-items: center;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.welcome-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.welcome-text {
|
||
font-size: 16px;
|
||
color: #6b7280;
|
||
text-align: center;
|
||
line-height: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.quick-actions {
|
||
width: 100%;
|
||
}
|
||
|
||
.quick-action {
|
||
width: 100%;
|
||
padding: 12px 20px;
|
||
margin-bottom: 8px;
|
||
background-color: #eff6ff;
|
||
border-radius: 20px;
|
||
border: 1px solid #dbeafe;
|
||
align-items: center;
|
||
}
|
||
|
||
.quick-text {
|
||
font-size: 14px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.messages-list {
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
.message-item {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.message-content {
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.message-user .message-content {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.message-assistant .message-content {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.message-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 16px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.user-avatar {
|
||
background-color: #3b82f6;
|
||
}
|
||
|
||
.assistant-avatar {
|
||
background-color: #10b981;
|
||
}
|
||
|
||
.avatar-text {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.message-bubble {
|
||
max-width: 70%;
|
||
padding: 12px 16px;
|
||
border-radius: 18px;
|
||
position: relative;
|
||
}
|
||
|
||
.user-bubble {
|
||
background-color: #3b82f6;
|
||
border-bottom-right-radius: 6px;
|
||
}
|
||
|
||
.assistant-bubble {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e5e5e5;
|
||
border-bottom-left-radius: 6px;
|
||
}
|
||
|
||
.message-text {
|
||
font-size: 16px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.user-bubble .message-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.assistant-bubble .message-text {
|
||
color: #1f2937;
|
||
}
|
||
|
||
.message-actions {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.action-item {
|
||
width: 24px;
|
||
height: 24px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 12px;
|
||
background-color: #f3f4f6;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.action-item .action-icon {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.user-bubble .message-time {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.typing-indicator {
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.typing-bubble {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 18px;
|
||
border-bottom-left-radius: 6px;
|
||
padding: 12px 16px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.typing-dots {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background-color: #9ca3af;
|
||
margin-right: 4px;
|
||
animation: typing 1.5s infinite;
|
||
}
|
||
|
||
.dot:nth-child(2) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
.dot:nth-child(3) {
|
||
animation-delay: 0.4s;
|
||
margin-right: 0;
|
||
}
|
||
|
||
@keyframes typing {
|
||
|
||
0%,
|
||
60%,
|
||
100% {
|
||
opacity: 0.3;
|
||
}
|
||
|
||
30% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.chat-input-section {
|
||
background-color: #ffffff;
|
||
border-top: 1px solid #e5e5e5;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.input-container {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-wrapper {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #f3f4f6;
|
||
border-radius: 24px;
|
||
padding: 0 16px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.chat-input {
|
||
flex: 1;
|
||
height: 44px;
|
||
font-size: 16px;
|
||
color: #374151;
|
||
}
|
||
|
||
.input-actions {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-actions .action-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 14px;
|
||
margin: 0;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.input-actions .action-icon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.send-btn {
|
||
width: 44px;
|
||
height: 44px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 22px;
|
||
background-color: #3b82f6;
|
||
}
|
||
|
||
.send-btn.disabled {
|
||
background-color: #d1d5db;
|
||
}
|
||
|
||
.send-icon {
|
||
font-size: 20px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.send-btn.disabled .send-icon {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.quick-modal,
|
||
.session-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.quick-content,
|
||
.session-content {
|
||
width: 320px;
|
||
max-height: 500px;
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
margin: 20px;
|
||
}
|
||
|
||
.modal-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.close-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 16px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.close-text {
|
||
font-size: 16px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.quick-list {
|
||
max-height: 400px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.quick-category {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.category-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.category-items {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.quick-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-right: 12px;
|
||
margin-bottom: 8px;
|
||
padding: 8px 12px;
|
||
background-color: #f3f4f6;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.quick-icon {
|
||
font-size: 16px;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.quick-label {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.session-list {
|
||
max-height: 300px;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.session-item {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.session-item.active {
|
||
background-color: #eff6ff;
|
||
margin: 0 -16px;
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.session-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.session-name {
|
||
font-size: 16px;
|
||
color: #1f2937;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.session-time {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.session-stats {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.session-count {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.session-actions {
|
||
padding: 16px;
|
||
border-top: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
width: 100%;
|
||
height: 44px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: #f3f4f6;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 16px;
|
||
color: #374151;
|
||
}
|
||
</style> |