refactor: 标题生成重构、UI样式优化、测试增强

- 将标题生成逻辑提取为独立函数,提前到Agent调用前非阻塞执行

- 修复模型/供应商不存在时的HTTP状态码 500→400

- ChatPanel: 分离模型选择useEffect、CSS类替代内联样式、按钮样式统一

- use-conversations: fetchConversations/fetchMessages改用handleResponse去重

- 聊天面板滚动优化(scroll-behavior: smooth, overflow-anchor: auto)

- 测试: mock支持onFinish回调,新增首次消息标题生成测试

- 移除未使用的SendMessageRequest接口
This commit is contained in:
2026-06-01 10:49:38 +08:00
parent f34028368d
commit 897fad95eb
6 changed files with 170 additions and 92 deletions

View File

@@ -70,21 +70,11 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
setInput("");
setMessages([]);
try {
const convPromise = fetchConversation(projectId, conversationId);
const msgPromise = fetchRef.current(projectId, conversationId);
const conv = await convPromise;
const data = await msgPromise;
if (cancelled) return;
const firstTextId = textModels[0]?.id;
if (firstTextId && textModels.every((m) => m.id !== conv.modelId)) {
setSelectedModelId(firstTextId);
void updateConversation(projectId, conversationId, { modelId: firstTextId });
} else {
setSelectedModelId(conv.modelId);
}
const history = data.items
.filter((m: { role: string }) => m.role === "user" || m.role === "assistant")
.reverse()
@@ -109,7 +99,22 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
return () => {
cancelled = true;
};
}, [conversationId, projectId, setMessages, message, textModels]);
}, [conversationId, projectId, setMessages, message]);
useEffect(() => {
if (!conversationId) return;
const firstTextId = textModels[0]?.id;
if (!firstTextId) return;
void fetchConversation(projectId, conversationId).then((conv) => {
if (textModels.every((m) => m.id !== conv.modelId)) {
setSelectedModelId(firstTextId);
void updateConversation(projectId, conversationId, { modelId: firstTextId });
} else {
setSelectedModelId(conv.modelId);
}
});
}, [conversationId, textModels, projectId]);
useEffect(() => {
scrollRef.current?.scrollTo({ behavior: "smooth", top: scrollRef.current.scrollHeight });
@@ -213,18 +218,39 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
const buttons: React.ReactNode[] = [];
buttons.push(
<Button icon={<CopyOutlined />} key="copy" onClick={() => handleCopy(msg)} size="small" type="text" />,
<Button
className="btn-dimmed"
icon={<CopyOutlined />}
key="copy"
onClick={() => handleCopy(msg)}
size="small"
type="text"
/>,
);
if (isLastUser && !isEditing) {
buttons.push(
<Button icon={<EditOutlined />} key="edit" onClick={() => handleEditStart(msg)} size="small" type="text" />,
<Button
className="btn-dimmed"
icon={<EditOutlined />}
key="edit"
onClick={() => handleEditStart(msg)}
size="small"
type="text"
/>,
);
}
if (isLastAssistant) {
buttons.push(
<Button icon={<RedoOutlined />} key="regenerate" onClick={handleRegenerate} size="small" type="text" />,
<Button
className="btn-dimmed"
icon={<RedoOutlined />}
key="regenerate"
onClick={handleRegenerate}
size="small"
type="text"
/>,
);
}
@@ -238,8 +264,8 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<div className="app-chat-panel">
<div className="chat-welcome-area">
<Flex align="center" gap={12} vertical>
<RobotOutlined style={{ color: "var(--ant-color-primary)", fontSize: 48 }} />
<Typography.Title level={3} style={{ margin: 0 }}>
<RobotOutlined className="welcome-icon" />
<Typography.Title className="welcome-title" level={3}>
</Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
@@ -274,6 +300,7 @@ export function ChatPanel({ conversationId, onConversationCreated, projectId }:
<Flex gap={8} vertical>
{messages.map((msg, idx) => (
<Card
classNames={{ extra: "card-extra-actions" }}
extra={getCardExtra(msg, idx)}
key={msg.id}
size="small"

View File

@@ -29,20 +29,12 @@ export async function fetchConversation(projectId: string, conversationId: strin
export async function fetchConversations(projectId: string): Promise<ConversationListResponse> {
const response = await fetch(`/api/projects/${projectId}/conversations?pageSize=100`);
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
return response.json() as Promise<ConversationListResponse>;
return handleResponse(response, (data) => data as ConversationListResponse);
}
export async function fetchMessages(projectId: string, conversationId: string): Promise<MessageListResponse> {
const response = await fetch(`/api/projects/${projectId}/conversations/${conversationId}/messages?pageSize=200`);
if (!response.ok) {
const body = (await response.json().catch(() => null)) as null | { error?: string };
throw new Error(body?.error ?? `HTTP ${response.status}`);
}
return response.json() as Promise<MessageListResponse>;
return handleResponse(response, (data) => data as MessageListResponse);
}
export async function updateConversation(

View File

@@ -184,7 +184,9 @@ body {
flex: 1;
min-height: 0;
overflow: auto;
overflow-anchor: auto;
padding-left: var(--ant-padding-sm);
scroll-behavior: smooth;
}
.chat-loading-indicator {
@@ -228,14 +230,23 @@ body {
color: var(--ant-color-error);
}
.welcome-icon {
color: var(--ant-color-primary);
font-size: 48px;
}
.welcome-title {
margin: 0;
}
.chat-model-select {
width: 180px;
}
.ant-card-extra .ant-btn-text {
.card-extra-actions .btn-dimmed {
color: var(--ant-color-text-quaternary);
}
.ant-card-extra .ant-btn-text:hover {
.card-extra-actions .btn-dimmed:hover {
color: var(--ant-color-text-secondary);
}