feat & fix: 新增打个招呼选项 创建新会话;修复ai重复性提问的问题;修复输入键盘覆盖对话气泡的问题

This commit is contained in:
yangshilin
2026-03-11 14:39:39 +08:00
parent 4b4dea8a45
commit 4d2c31b5a6
9 changed files with 309 additions and 59 deletions

View File

@@ -12,7 +12,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from services.llm_service import llm_service from services.llm_service import llm_service
from services.redis_service import redis_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 ( from .prompts.profile_prompts import (
get_profile_greeting_prompt, get_profile_greeting_prompt,
get_profile_extraction_prompt, get_profile_extraction_prompt,
@@ -20,6 +20,7 @@ from .prompts.profile_prompts import (
format_user_profile_context, format_user_profile_context,
get_missing_profile_fields, get_missing_profile_fields,
) )
from .prompts.conversation_prompts import SLOT_NAME_MAP
from .state_schema import MemoirStateSchema from .state_schema import MemoirStateSchema
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -138,19 +139,83 @@ class ConversationAgent:
logger.error(f"生成资料收集开场白失败: {e}") logger.error(f"生成资料收集开场白失败: {e}")
return ["你好!在我们开始聊人生故事之前,能先简单介绍一下你自己吗?比如你是哪一年出生的?"] 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: if not self.llm or not missing_fields:
return {} 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: 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) response = await self.llm.ainvoke(prompt)
content = response.content.strip() content = response.content.strip()
parsed = json.loads(content) parsed = json.loads(content)
result = {} result = {}
if "birth_year" in parsed and isinstance(parsed["birth_year"], int): if "birth_year" in parsed and parsed["birth_year"] is not None:
result["birth_year"] = parsed["birth_year"] 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"]: if "birth_place" in parsed and parsed["birth_place"]:
result["birth_place"] = str(parsed["birth_place"]) result["birth_place"] = str(parsed["birth_place"])
if "grew_up_place" in parsed and parsed["grew_up_place"]: if "grew_up_place" in parsed and parsed["grew_up_place"]:

View File

@@ -6,6 +6,7 @@ from .conversation_prompts import (
get_system_prompt as get_conversation_prompt, get_system_prompt as get_conversation_prompt,
get_questions_for_stage, get_questions_for_stage,
get_guided_conversation_prompt, get_guided_conversation_prompt,
get_opening_prompt,
INTERVIEW_QUESTIONS, INTERVIEW_QUESTIONS,
) )
from .memory_prompts import ( from .memory_prompts import (
@@ -34,6 +35,7 @@ __all__ = [
"get_conversation_prompt", "get_conversation_prompt",
"get_questions_for_stage", "get_questions_for_stage",
"get_guided_conversation_prompt", "get_guided_conversation_prompt",
"get_opening_prompt",
"INTERVIEW_QUESTIONS", "INTERVIEW_QUESTIONS",
"get_memory_prompt", "get_memory_prompt",
"get_chapter_classification_prompt", "get_chapter_classification_prompt",

View File

@@ -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: def _build_era_context(current_stage: str, user_profile_context: str) -> str:
""" """
根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示。 根据用户的人生阶段和出生年份,生成对应时代的历史/政治/文化背景提示。
@@ -402,6 +443,7 @@ def get_guided_conversation_prompt(
- 禁止反复追问同一件事 - 禁止反复追问同一件事
- 禁止每次都以问题结尾 - 禁止每次都以问题结尾
- **禁止在用户聊别的话题时强行拉回之前的话题** - **禁止在用户聊别的话题时强行拉回之前的话题**
- **禁止询问或再次确认「用户基本信息」中已列出的内容**(如出生年份、出生地、成长地、职业等,这些你已经知道,不要问第二遍)
## 好的回应示例 ## 好的回应示例
- "哈哈,你这说的让我想起..."(轻松) - "哈哈,你这说的让我想起..."(轻松)

View File

@@ -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} 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}" "{user_message}"
需要提取的字段(只提取确实提到的): 需要提取的字段(只提取确实在对话中出现过的):
{missing_names} {missing_names}
请返回 JSON 格式,只包含确实提到的字段: 请返回 JSON 格式,只包含确实提到的字段:
@@ -69,8 +78,8 @@ def get_profile_extraction_prompt(user_message: str, missing_fields: List[str])
}} }}
规则: 规则:
1. birth_year 必须是整数(四位数年份),如"65年出生"转为 1965 1. birth_year 整数(四位数),如"65年出生"转为 1965
2. 如果用户"在老家长大"而之前提到了出生地grew_up_place 可以和 birth_place 相同 2. 如果用户在任一轮说过出生地/成长地/职业等,都要提取
3. 只提取明确提到的信息,不要猜测 3. 只提取明确提到的信息,不要猜测
4. 如果没有提取到任何信息,返回空对象 {{}} 4. 如果没有提取到任何信息,返回空对象 {{}}
@@ -107,17 +116,19 @@ def get_profile_followup_prompt(
return f"""你是「岁月知己」,正在和用户聊天收集基本信息。 return f"""你是「岁月知己」,正在和用户聊天收集基本信息。
已知信息 ## 已知信息(严禁再次询问以下任何一项)
{filled_str} {filled_str}
还需要了解{missing_str} ## 还需要了解
{missing_str}
用户刚才说:"{user_message}" 用户刚才说:"{user_message}"
请先对用户说的内容做出自然回应,然后继续询问还未了解的信息(每次问 1-2 个)。 请先对用户说的内容做出自然回应,然后**只**询问「还需要了解」中的信息(每次问 1-2 个)。
语气要像朋友聊天一样自然亲切。 语气要像朋友聊天一样自然亲切。
严格禁止: 严格禁止:
- **严禁再次询问「已知信息」中已列出的内容**(例如已知出生年份就绝不要再问哪年出生)
- 禁止输出括号注释、思考过程 - 禁止输出括号注释、思考过程
- 禁止说"我注意到""我需要了解" - 禁止说"我注意到""我需要了解"

View File

@@ -22,7 +22,8 @@ from database.models import Conversation, Segment
from database.models import User as UserModel from database.models import User as UserModel
from services.auth_service import verify_token from services.auth_service import verify_token
from services.memoir_state_service import get_or_create_state 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__) logger = logging.getLogger(__name__)
LEGACY_VOICE_SESSION_ID = "legacy" LEGACY_VOICE_SESSION_ID = "legacy"
@@ -498,6 +499,35 @@ async def websocket_endpoint(
await _asyncio_greet.sleep(0.5) await _asyncio_greet.sleep(0.5)
except Exception as e: except Exception as e:
logger.error(f"发送资料收集开场白失败: {e}", exc_info=True) 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: while True:
@@ -889,7 +919,9 @@ async def process_user_message(
missing = _get_missing_profile_fields(user) missing = _get_missing_profile_fields(user)
if missing: if missing:
try: 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: if extracted:
await _apply_extracted_profile(user, extracted, db) await _apply_extracted_profile(user, extracted, db)

View File

@@ -11,9 +11,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.ui.theme.LightPurple
import com.huaga.life_echo.utils.TimeUtils import com.huaga.life_echo.utils.TimeUtils
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** /**
* 消息列表组件 * 消息列表组件
@@ -52,7 +58,9 @@ fun MessageList(
audioDurations: Map<String, Int> = emptyMap() // messageId -> 时长(秒) audioDurations: Map<String, Int> = emptyMap() // messageId -> 时长(秒)
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
var lastHeightPx by remember { mutableIntStateOf(0) }
// 计算实际的列表项数量(考虑分割消息和附加项) // 计算实际的列表项数量(考虑分割消息和附加项)
val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) { val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) {
var count = 0 var count = 0
@@ -110,7 +118,25 @@ fun MessageList(
LazyColumn( LazyColumn(
state = listState, 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), contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {

View File

@@ -14,12 +14,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -37,8 +40,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel 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.Cream
import com.huaga.life_echo.ui.theme.DeepPurple import com.huaga.life_echo.ui.theme.DeepPurple
import com.huaga.life_echo.ui.theme.DividerColor 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.MediumPurple
import com.huaga.life_echo.ui.theme.SlatePurple import com.huaga.life_echo.ui.theme.SlatePurple
import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel import com.huaga.life_echo.ui.viewmodel.ConversationListViewModel
@@ -77,33 +83,11 @@ fun ConversationListScreen(
var isSelectionMode by remember { mutableStateOf(false) } var isSelectionMode by remember { mutableStateOf(false) }
var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) } var selectedIds by remember { mutableStateOf(mutableSetOf<String>()) }
// 是否正在自动创建对话 // 是否正在创建对话(点击「打个招呼」)
var isAutoCreating by remember { mutableStateOf(false) } var isCreating by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() 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 -> val handleLongClick: (String) -> Unit = { conversationId ->
if (!isSelectionMode) { if (!isSelectionMode) {
@@ -192,22 +176,43 @@ fun ConversationListScreen(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
conversations.isEmpty() -> {
if (isAutoCreating) {
LoadingIndicator()
} else {
EmptyStateView(
title = "正在初始化",
message = "正在为您准备回忆录对话...",
modifier = Modifier.fillMaxSize()
)
}
}
else -> { else -> {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = AppDimensions.screenPadding) 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 { item {
Text( Text(
@@ -224,7 +229,7 @@ fun ConversationListScreen(
// 对话列表 // 对话列表
items(conversations, key = { it.id }) { conversation -> items(conversations, key = { it.id }) { conversation ->
// 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手 // 兼容新旧数据:将"岁月知己"和旧名称"回忆录助手"都识别为默认助手,展示为「岁月知己」
val isAssistant = conversation.title == null || conversation.title == "岁月知己" || conversation.title == "回忆录助手" val isAssistant = conversation.title == null || conversation.title == "岁月知己" || conversation.title == "回忆录助手"
val displayTitle = if (isAssistant) "岁月知己" else conversation.title!! val displayTitle = if (isAssistant) "岁月知己" else conversation.title!!
val dto = ConversationListItemDto( 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)
)
}
}
}
/** /**
* 多选模式头部 * 多选模式头部
*/ */

View File

@@ -152,7 +152,12 @@ fun CreateMemoryScreen(
contentWindowInsets = WindowInsets(0, 0, 0, 0), contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.imePadding()
) {
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -200,7 +205,6 @@ fun CreateMemoryScreen(
// 使用新的ChatInputField组件支持语音输入 // 使用新的ChatInputField组件支持语音输入
ChatInputField( ChatInputField(
modifier = Modifier.imePadding(),
value = inputText, value = inputText,
onValueChange = { onValueChange = {
conversationDrafts = ConversationDrafts.updateDraft( conversationDrafts = ConversationDrafts.updateDraft(

View File

@@ -21,7 +21,8 @@
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false "predictiveBackGestureEnabled": false,
"softwareKeyboardLayoutMode": "resize"
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"