feat & fix: 新增打个招呼选项 创建新会话;修复ai重复性提问的问题;修复输入键盘覆盖对话气泡的问题
This commit is contained in:
@@ -12,7 +12,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
|
||||
from services.llm_service import llm_service
|
||||
from services.redis_service import redis_service
|
||||
from .prompts import ConversationStage, get_conversation_prompt, get_guided_conversation_prompt
|
||||
from .prompts import ConversationStage, get_conversation_prompt, get_guided_conversation_prompt, get_opening_prompt
|
||||
from .prompts.profile_prompts import (
|
||||
get_profile_greeting_prompt,
|
||||
get_profile_extraction_prompt,
|
||||
@@ -20,6 +20,7 @@ from .prompts.profile_prompts import (
|
||||
format_user_profile_context,
|
||||
get_missing_profile_fields,
|
||||
)
|
||||
from .prompts.conversation_prompts import SLOT_NAME_MAP
|
||||
from .state_schema import MemoirStateSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -138,19 +139,83 @@ class ConversationAgent:
|
||||
logger.error(f"生成资料收集开场白失败: {e}")
|
||||
return ["你好!在我们开始聊人生故事之前,能先简单介绍一下你自己吗?比如你是哪一年出生的?"]
|
||||
|
||||
async def extract_profile_from_message(self, user_message: str, missing_fields: List[str]) -> Dict[str, Any]:
|
||||
"""从用户消息中提取基础资料信息"""
|
||||
async def generate_opening_message(
|
||||
self,
|
||||
conversation_id: str,
|
||||
memoir_state: MemoirStateSchema,
|
||||
user_profile_context: str = "",
|
||||
) -> List[str]:
|
||||
"""
|
||||
空对话时 AI 先开口:用户通过「打个招呼」进入,尚未发任何消息。
|
||||
生成问候 + 一个引导问题,写入 Redis 并返回消息列表。
|
||||
"""
|
||||
if not self.llm:
|
||||
return ["你好呀~ 有空聊聊你的人生故事吗?你小时候是在哪儿长大的?"]
|
||||
|
||||
try:
|
||||
empty_slots = memoir_state.empty_slots_for_current_stage()
|
||||
empty_slots_readable = [SLOT_NAME_MAP.get(s, s) for s in empty_slots]
|
||||
if not empty_slots_readable:
|
||||
empty_slots_readable = ["成长的地方", "难忘的事", "重要的人"]
|
||||
|
||||
prompt = get_opening_prompt(
|
||||
current_stage=memoir_state.current_stage,
|
||||
empty_slots_readable=empty_slots_readable,
|
||||
user_profile_context=user_profile_context,
|
||||
)
|
||||
full_prompt = f"{prompt}\n\nAssistant:"
|
||||
response = await self.llm.ainvoke(full_prompt)
|
||||
response_text = response.content if hasattr(response, "content") else str(response)
|
||||
|
||||
await self._save_message(conversation_id, "ai", response_text)
|
||||
|
||||
messages = [msg.strip() for msg in response_text.split("[SPLIT]") if msg.strip()]
|
||||
return messages[:3] if messages else [response_text]
|
||||
except Exception as e:
|
||||
logger.error(f"生成开场白失败: {e}", exc_info=True)
|
||||
return ["你好呀~ 有空聊聊你的人生故事吗?你童年里印象最深的一件事是什么?"]
|
||||
|
||||
async def extract_profile_from_message(
|
||||
self,
|
||||
user_message: str,
|
||||
missing_fields: List[str],
|
||||
conversation_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""从用户消息中提取基础资料信息;若提供 conversation_id,会结合最近几轮对话一起提取,避免漏提。"""
|
||||
if not self.llm or not missing_fields:
|
||||
return {}
|
||||
|
||||
recent_dialogue = ""
|
||||
if conversation_id:
|
||||
history_messages = await self._get_history_messages(conversation_id)
|
||||
# 取最近 4 条(2 轮),不包含本轮;本轮由 user_message 单独传入
|
||||
recent = history_messages[-4:] if len(history_messages) > 4 else history_messages
|
||||
parts = []
|
||||
for msg in recent:
|
||||
if isinstance(msg, HumanMessage):
|
||||
parts.append(f"用户: {msg.content}")
|
||||
elif isinstance(msg, AIMessage):
|
||||
parts.append(f"助手: {msg.content}")
|
||||
recent_dialogue = "\n".join(parts) if parts else ""
|
||||
|
||||
try:
|
||||
prompt = get_profile_extraction_prompt(user_message, missing_fields)
|
||||
prompt = get_profile_extraction_prompt(
|
||||
user_message, missing_fields, recent_dialogue=recent_dialogue or None
|
||||
)
|
||||
response = await self.llm.ainvoke(prompt)
|
||||
content = response.content.strip()
|
||||
parsed = json.loads(content)
|
||||
result = {}
|
||||
if "birth_year" in parsed and isinstance(parsed["birth_year"], int):
|
||||
result["birth_year"] = parsed["birth_year"]
|
||||
if "birth_year" in parsed and parsed["birth_year"] is not None:
|
||||
raw = parsed["birth_year"]
|
||||
if isinstance(raw, int) and 1900 <= raw <= 2100:
|
||||
result["birth_year"] = raw
|
||||
elif isinstance(raw, str) and raw.isdigit():
|
||||
y = int(raw)
|
||||
if y < 100: # "65" -> 1965
|
||||
y = 1900 + y if y >= 50 else 2000 + y
|
||||
if 1900 <= y <= 2100:
|
||||
result["birth_year"] = y
|
||||
if "birth_place" in parsed and parsed["birth_place"]:
|
||||
result["birth_place"] = str(parsed["birth_place"])
|
||||
if "grew_up_place" in parsed and parsed["grew_up_place"]:
|
||||
|
||||
@@ -6,6 +6,7 @@ from .conversation_prompts import (
|
||||
get_system_prompt as get_conversation_prompt,
|
||||
get_questions_for_stage,
|
||||
get_guided_conversation_prompt,
|
||||
get_opening_prompt,
|
||||
INTERVIEW_QUESTIONS,
|
||||
)
|
||||
from .memory_prompts import (
|
||||
@@ -34,6 +35,7 @@ __all__ = [
|
||||
"get_conversation_prompt",
|
||||
"get_questions_for_stage",
|
||||
"get_guided_conversation_prompt",
|
||||
"get_opening_prompt",
|
||||
"INTERVIEW_QUESTIONS",
|
||||
"get_memory_prompt",
|
||||
"get_chapter_classification_prompt",
|
||||
|
||||
@@ -173,6 +173,47 @@ RESPONSE_STYLES = [
|
||||
]
|
||||
|
||||
|
||||
def get_opening_prompt(
|
||||
current_stage: str,
|
||||
empty_slots_readable: List[str],
|
||||
user_profile_context: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
空对话时 AI 先开口的提示词(用户通过「打个招呼」进入,尚未发送任何消息)。
|
||||
要求 AI 先发一条问候 + 一个具体问题,引导用户开始分享。
|
||||
"""
|
||||
stage_name_map = {
|
||||
"childhood": "童年时光",
|
||||
"education": "求学经历",
|
||||
"career": "职业生涯",
|
||||
"family": "家庭生活",
|
||||
"belief": "人生信念",
|
||||
}
|
||||
stage_name = stage_name_map.get(current_stage, current_stage)
|
||||
topics_str = "、".join(empty_slots_readable) if empty_slots_readable else "人生故事、童年、经历等"
|
||||
profile_section = f"\n## 用户基本信息\n{user_profile_context}\n" if user_profile_context else ""
|
||||
return f"""你是「岁月知己」,用户的老朋友。用户刚通过「打个招呼」进入空对话,**还没有发任何消息**,需要你先开口。
|
||||
{profile_section}
|
||||
## 当前建议话题({stage_name})
|
||||
可以从中选一个来问:{topics_str}
|
||||
|
||||
## 你的任务
|
||||
1. **先开口**:用一两句亲切的问候开场(如「你好呀,有空聊聊你的故事吗」)。
|
||||
2. **必须问一个问题**:接着问一个**具体、好回答**的问题,引导用户开始分享(如童年、家乡、印象深的事等)。不要问太宽泛的「有什么想聊的」。
|
||||
3. 语气像老朋友,自然、温暖。
|
||||
|
||||
## 回复格式
|
||||
- 可以分成 2 条消息,用 [SPLIT] 分隔:第一条问候,第二条问题;或合并成一条「问候 + 问题」。
|
||||
- 禁止输出括号、注释、思考过程。
|
||||
|
||||
示例(仅供参考风格):
|
||||
"你好呀~ 有空的话想听听你的人生故事。你小时候是在哪儿长大的?那边有什么特别让你怀念的?"
|
||||
或
|
||||
"在的!今天想聊聊你。你童年里印象最深的一件事是什么?"
|
||||
|
||||
直接输出你要说的话(多条用 [SPLIT] 分隔):"""
|
||||
|
||||
|
||||
def _build_era_context(current_stage: str, user_profile_context: str) -> str:
|
||||
"""
|
||||
根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示。
|
||||
@@ -402,6 +443,7 @@ def get_guided_conversation_prompt(
|
||||
- 禁止反复追问同一件事
|
||||
- 禁止每次都以问题结尾
|
||||
- **禁止在用户聊别的话题时强行拉回之前的话题**
|
||||
- **禁止询问或再次确认「用户基本信息」中已列出的内容**(如出生年份、出生地、成长地、职业等,这些你已经知道,不要问第二遍)
|
||||
|
||||
## 好的回应示例
|
||||
- "哈哈,你这说的让我想起..."(轻松)
|
||||
|
||||
@@ -48,16 +48,25 @@ def get_profile_greeting_prompt(missing_fields: List[str], nickname: str = "") -
|
||||
直接输出你要说的话:"""
|
||||
|
||||
|
||||
def get_profile_extraction_prompt(user_message: str, missing_fields: List[str]) -> str:
|
||||
"""从用户回答中提取基础资料信息"""
|
||||
def get_profile_extraction_prompt(
|
||||
user_message: str,
|
||||
missing_fields: List[str],
|
||||
recent_dialogue: Optional[str] = None,
|
||||
) -> str:
|
||||
"""从用户回答中提取基础资料信息(可包含最近几轮对话,避免漏提)"""
|
||||
missing_names = {f: PROFILE_FIELD_NAMES[f] for f in missing_fields if f in PROFILE_FIELD_NAMES}
|
||||
|
||||
return f"""请从用户的回答中提取基础资料信息。
|
||||
dialogue_section = ""
|
||||
if recent_dialogue and recent_dialogue.strip():
|
||||
dialogue_section = f"""
|
||||
最近几轮对话(可从用户任一轮回答中提取):
|
||||
{recent_dialogue.strip()}
|
||||
|
||||
用户的回答:
|
||||
"""
|
||||
return f"""请从以下内容中提取用户已提到的基础资料信息。{dialogue_section}用户本轮回答:
|
||||
"{user_message}"
|
||||
|
||||
需要提取的字段(只提取确实提到的):
|
||||
需要提取的字段(只提取确实在对话中出现过的):
|
||||
{missing_names}
|
||||
|
||||
请返回 JSON 格式,只包含确实提到的字段:
|
||||
@@ -69,8 +78,8 @@ def get_profile_extraction_prompt(user_message: str, missing_fields: List[str])
|
||||
}}
|
||||
|
||||
规则:
|
||||
1. birth_year 必须是整数(四位数年份),如"65年出生"应转为 1965
|
||||
2. 如果用户说"在老家长大"而之前提到了出生地,grew_up_place 可以和 birth_place 相同
|
||||
1. birth_year 填整数(四位数),如"65年出生"转为 1965
|
||||
2. 如果用户在任一轮说过出生地/成长地/职业等,都要提取
|
||||
3. 只提取明确提到的信息,不要猜测
|
||||
4. 如果没有提取到任何信息,返回空对象 {{}}
|
||||
|
||||
@@ -107,17 +116,19 @@ def get_profile_followup_prompt(
|
||||
|
||||
return f"""你是「岁月知己」,正在和用户聊天收集基本信息。
|
||||
|
||||
已知信息:
|
||||
## 已知信息(严禁再次询问以下任何一项)
|
||||
{filled_str}
|
||||
|
||||
还需要了解:{missing_str}
|
||||
## 还需要了解
|
||||
{missing_str}
|
||||
|
||||
用户刚才说:"{user_message}"
|
||||
|
||||
请先对用户说的内容做出自然回应,然后继续询问还未了解的信息(每次问 1-2 个)。
|
||||
请先对用户说的内容做出自然回应,然后**只**询问「还需要了解」中的信息(每次问 1-2 个)。
|
||||
语气要像朋友聊天一样自然亲切。
|
||||
|
||||
严格禁止:
|
||||
- **严禁再次询问「已知信息」中已列出的内容**(例如已知出生年份就绝不要再问哪年出生)
|
||||
- 禁止输出括号注释、思考过程
|
||||
- 禁止说"我注意到""我需要了解"
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ from database.models import Conversation, Segment
|
||||
from database.models import User as UserModel
|
||||
from services.auth_service import verify_token
|
||||
from services.memoir_state_service import get_or_create_state
|
||||
from services import asr_service
|
||||
from services import asr_service, redis_service
|
||||
from agents.prompts.profile_prompts import format_user_profile_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LEGACY_VOICE_SESSION_ID = "legacy"
|
||||
@@ -498,6 +499,35 @@ async def websocket_endpoint(
|
||||
await _asyncio_greet.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.error(f"发送资料收集开场白失败: {e}", exc_info=True)
|
||||
else:
|
||||
# 资料已完整:若为空对话(用户通过「打个招呼」进入),AI 先开口提问
|
||||
history = await redis_service.get_conversation_history(conversation_id)
|
||||
if not history:
|
||||
try:
|
||||
state = await get_or_create_state(user_id, db)
|
||||
user_profile_context = format_user_profile_context(
|
||||
birth_year=user.birth_year,
|
||||
birth_place=user.birth_place,
|
||||
grew_up_place=user.grew_up_place,
|
||||
occupation=user.occupation,
|
||||
)
|
||||
opening_messages = await manager.conversation_agent.generate_opening_message(
|
||||
conversation_id=conversation_id,
|
||||
memoir_state=state,
|
||||
user_profile_context=user_profile_context,
|
||||
)
|
||||
import asyncio as _asyncio_open
|
||||
for i, text in enumerate(opening_messages):
|
||||
await manager.send_message(conversation_id, {
|
||||
"type": MessageType.AGENT_RESPONSE,
|
||||
"conversation_id": conversation_id,
|
||||
"data": {"text": text, "index": i, "total": len(opening_messages)},
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
})
|
||||
if i < len(opening_messages) - 1:
|
||||
await _asyncio_open.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.error(f"发送空对话开场白失败: {e}", exc_info=True)
|
||||
|
||||
# 主循环:处理消息
|
||||
while True:
|
||||
@@ -889,7 +919,9 @@ async def process_user_message(
|
||||
missing = _get_missing_profile_fields(user)
|
||||
if missing:
|
||||
try:
|
||||
extracted = await agent.extract_profile_from_message(user_message, missing)
|
||||
extracted = await agent.extract_profile_from_message(
|
||||
user_message, missing, conversation_id=conversation_id
|
||||
)
|
||||
if extracted:
|
||||
await _apply_extracted_profile(user, extracted, db)
|
||||
|
||||
|
||||
@@ -11,9 +11,14 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -25,6 +30,7 @@ import com.huaga.life_echo.ui.theme.AppTypography
|
||||
import com.huaga.life_echo.ui.theme.LightPurple
|
||||
import com.huaga.life_echo.utils.TimeUtils
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 消息列表组件
|
||||
@@ -52,7 +58,9 @@ fun MessageList(
|
||||
audioDurations: Map<String, Int> = emptyMap() // messageId -> 时长(秒)
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var lastHeightPx by remember { mutableIntStateOf(0) }
|
||||
|
||||
// 计算实际的列表项数量(考虑分割消息和附加项)
|
||||
val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) {
|
||||
var count = 0
|
||||
@@ -110,7 +118,25 @@ fun MessageList(
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
val h = size.height
|
||||
// 键盘弹出导致列表高度变矮时,滚到最底部,让最后一条气泡紧贴输入框上方
|
||||
if (lastHeightPx > 0 && h < lastHeightPx) {
|
||||
scope.launch {
|
||||
delay(80)
|
||||
val count = listState.layoutInfo.totalItemsCount
|
||||
if (count > 0) {
|
||||
val viewportHeight = listState.layoutInfo.viewportSize.height
|
||||
// 一次平滑滚到底:用 scrollOffset 让最后一项贴底,避免两段滚动和中间停顿
|
||||
val scrollOffset = (viewportHeight - 120).coerceAtLeast(0)
|
||||
listState.animateScrollToItem(count - 1, scrollOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
lastHeightPx = h
|
||||
},
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
|
||||
@@ -14,12 +14,15 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -37,8 +40,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -54,6 +59,7 @@ import com.huaga.life_echo.ui.theme.AppWhite
|
||||
import com.huaga.life_echo.ui.theme.Cream
|
||||
import com.huaga.life_echo.ui.theme.DeepPurple
|
||||
import com.huaga.life_echo.ui.theme.DividerColor
|
||||
import com.huaga.life_echo.ui.theme.Lavender
|
||||
import com.huaga.life_echo.ui.theme.MediumPurple
|
||||
import com.huaga.life_echo.ui.theme.SlatePurple
|
||||
import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel
|
||||
@@ -77,33 +83,11 @@ fun ConversationListScreen(
|
||||
var isSelectionMode by remember { mutableStateOf(false) }
|
||||
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
|
||||
|
||||
// 是否正在自动创建对话
|
||||
var isAutoCreating by remember { mutableStateOf(false) }
|
||||
// 是否正在创建新对话(点击「打个招呼」)
|
||||
var isCreating by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 加载完成后,当对话列表为空时,自动创建一个对话并进入
|
||||
LaunchedEffect(conversations, isLoading, isAutoCreating, hasLoadedInitialConversations, error) {
|
||||
if (
|
||||
hasLoadedInitialConversations &&
|
||||
!isLoading &&
|
||||
error == null &&
|
||||
conversations.isEmpty() &&
|
||||
!isAutoCreating
|
||||
) {
|
||||
isAutoCreating = true
|
||||
val result = viewModel.createConversation()
|
||||
result.fold(
|
||||
onSuccess = { conversationId ->
|
||||
onConversationClick(conversationId)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
isAutoCreating = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理长按进入多选模式
|
||||
val handleLongClick: (String) -> Unit = { conversationId ->
|
||||
if (!isSelectionMode) {
|
||||
@@ -192,22 +176,43 @@ fun ConversationListScreen(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
conversations.isEmpty() -> {
|
||||
if (isAutoCreating) {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
EmptyStateView(
|
||||
title = "正在初始化",
|
||||
message = "正在为您准备回忆录对话...",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = AppDimensions.screenPadding)
|
||||
) {
|
||||
// 置顶入口:打个招呼(点击即新建空会话)
|
||||
item(key = "say_hi") {
|
||||
SayHiEntry(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = !isCreating) {
|
||||
if (isCreating) return@clickable
|
||||
scope.launch {
|
||||
isCreating = true
|
||||
viewModel.createConversation()
|
||||
.fold(
|
||||
onSuccess = { conversationId ->
|
||||
onConversationClick(conversationId)
|
||||
},
|
||||
onFailure = { }
|
||||
)
|
||||
isCreating = false
|
||||
}
|
||||
}
|
||||
.padding(
|
||||
horizontal = AppDimensions.screenPadding,
|
||||
vertical = AppDimensions.itemSpacing
|
||||
),
|
||||
isLoading = isCreating
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = AppDimensions.screenPadding),
|
||||
thickness = AppDimensions.dividerThickness,
|
||||
color = DividerColor
|
||||
)
|
||||
}
|
||||
|
||||
// 区块标题(与章节正文字号一致,大字模式更易读)
|
||||
item {
|
||||
Text(
|
||||
@@ -224,7 +229,7 @@ fun ConversationListScreen(
|
||||
|
||||
// 对话列表
|
||||
items(conversations, key = { it.id }) { conversation ->
|
||||
// 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手
|
||||
// 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手,展示为「岁月知己」
|
||||
val isAssistant = conversation.title == null || conversation.title == "岁月知己" || conversation.title == "回忆录助手"
|
||||
val displayTitle = if (isAssistant) "岁月知己" else conversation.title!!
|
||||
val dto = ConversationListItemDto(
|
||||
@@ -284,6 +289,68 @@ fun ConversationListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 置顶入口:「打个招呼」— 点击即新建空会话
|
||||
*/
|
||||
@Composable
|
||||
private fun SayHiEntry(
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(AppDimensions.avatarSizeSmall)
|
||||
.clip(RoundedCornerShape(AppDimensions.iconContainerRadius))
|
||||
.background(Lavender),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Conversation,
|
||||
contentDescription = "打个招呼",
|
||||
tint = DeepPurple,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "打个招呼",
|
||||
fontSize = AppTypography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = DeepPurple,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = "开始新对话",
|
||||
fontSize = AppTypography.bodyMedium,
|
||||
color = SlatePurple,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MediumPurple,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = AppIcons.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = SlatePurple,
|
||||
modifier = Modifier.size(AppDimensions.iconSizeSmall)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选模式头部
|
||||
*/
|
||||
|
||||
@@ -152,7 +152,12 @@ fun CreateMemoryScreen(
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.imePadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@@ -200,7 +205,6 @@ fun CreateMemoryScreen(
|
||||
|
||||
// 使用新的ChatInputField组件(支持语音输入)
|
||||
ChatInputField(
|
||||
modifier = Modifier.imePadding(),
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
conversationDrafts = ConversationDrafts.updateDraft(
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"softwareKeyboardLayoutMode": "resize"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
|
||||
Reference in New Issue
Block a user