Files
akmon/pages/info/chat.uvue
2026-01-20 08:04:15 +08:00

1096 lines
26 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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>