fix: stabilize build and improve mobile chat layout

main
sp mac bookpro 2605 2026-06-11 13:50:08 +08:00
parent 423fc9531f
commit faef8b6aea
14 changed files with 364 additions and 250 deletions

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Button, Card, List, Modal, App as AntApp, Space, Alert, Tag, Popconfirm, Tooltip } from 'antd';
import { useEffect, useState } from 'react';
import { Button, Card, List, Modal, App as AntApp, Space, Alert, Tag, Popconfirm, Tooltip, Form, Input, Select } from 'antd';
import type { McpServer } from '../../../api';
import type { McpPanelLogicOutput } from '../McpPanelLogic';
@ -230,8 +230,7 @@ function McpFormH5({
onSubmit: (values: any) => void;
onCancel: () => void;
}) {
const { useForm } = AntApp;
const [form] = useForm();
const [form] = Form.useForm();
const [transport, setTransport] = useState<'stdio' | 'sse' | 'http'>(initial?.transport ?? 'stdio');
useEffect(() => {
@ -247,92 +246,50 @@ function McpFormH5({
}, [initial, form]);
return (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit(form.getFieldsValue());
}}>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}></label>
<input
{...form.getFieldProps('name')}
placeholder="例如 filesystem"
style={{ width: '100%', padding: '8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
/>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}>Transport</label>
<select
<Form form={form} layout="vertical" onFinish={onSubmit}>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="例如 filesystem" />
</Form.Item>
<Form.Item name="transport" label="Transport" rules={[{ required: true, message: '请选择 Transport' }]}>
<Select
value={transport}
onChange={(e) => setTransport(e.target.value as any)}
style={{ width: '100%', padding: '8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
>
<option value="stdio">stdio ()</option>
<option value="sse">SSE ()</option>
<option value="http">HTTP (Streamable)</option>
</select>
</div>
onChange={(v) => {
setTransport(v as any);
form.setFieldValue('transport', v);
}}
options={[
{ value: 'stdio', label: 'stdio (本机进程)' },
{ value: 'sse', label: 'SSE (远程)' },
{ value: 'http', label: 'HTTP (Streamable)' },
]}
/>
</Form.Item>
{transport === 'stdio' ? (
<>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}>Command</label>
<input
{...form.getFieldProps('command')}
placeholder="npx"
style={{ width: '100%', padding: '8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
/>
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}> npx / node / python</div>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}>Args ()</label>
<textarea
{...form.getFieldProps('argsText')}
placeholder={'-y\n@modelcontextprotocol/server-filesystem\n/path/to/dir'}
style={{
width: '100%',
minHeight: 60,
fontFamily: 'Consolas, monospace',
fontSize: 12,
padding: 8,
borderRadius: 6,
border: '1px solid #d9d9d9',
}}
/>
</div>
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}>Env (JSON )</label>
<textarea
{...form.getFieldProps('envText')}
placeholder={'{ "API_KEY": "xxx" }'}
style={{
width: '100%',
minHeight: 40,
fontFamily: 'Consolas, monospace',
fontSize: 12,
padding: 8,
borderRadius: 6,
border: '1px solid #d9d9d9',
}}
/>
</div>
<Form.Item name="command" label="Command">
<Input placeholder="npx" />
</Form.Item>
<Form.Item name="argsText" label="Args (每行一个)">
<Input.TextArea placeholder={'-y\n@modelcontextprotocol/server-filesystem\n/path/to/dir'} autoSize={{ minRows: 3, maxRows: 8 }} style={{ fontFamily: 'Consolas, monospace', fontSize: 12 }} />
</Form.Item>
<Form.Item name="envText" label="Env (JSON 对象)">
<Input.TextArea placeholder={'{ "API_KEY": "xxx" }'} autoSize={{ minRows: 2, maxRows: 8 }} style={{ fontFamily: 'Consolas, monospace', fontSize: 12 }} />
</Form.Item>
</>
) : (
<div style={{ marginBottom: 12 }}>
<label style={{ fontSize: 14, fontWeight: 500, marginBottom: 4, display: 'block' }}>URL</label>
<input
{...form.getFieldProps('url')}
placeholder="https://example.com/mcp"
style={{ width: '100%', padding: '8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
/>
</div>
<Form.Item name="url" label="URL" rules={[{ required: true, message: '请输入 URL' }]}>
<Input placeholder="https://example.com/mcp" />
</Form.Item>
)}
<Space style={{ justifyContent: 'flex-end', width: '100%', marginTop: 16 }}>
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</form>
</Form>
);
}

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { Button, Card, List, Modal, App as AntApp, Space, Alert, Tag, Popconfirm, Tooltip } from 'antd';
import { useEffect, useState } from 'react';
import { Button, Card, List, Modal, App as AntApp, Space, Alert, Tag, Popconfirm, Tooltip, Form, Input, Select } from 'antd';
import type { McpServer } from '../../../api';
import type { McpPanelLogicOutput } from '../McpPanelLogic';
@ -219,7 +219,7 @@ function McpForm({
onSubmit: (values: any) => void;
onCancel: () => void;
}) {
const [form] = AntApp.useApp().form;
const [form] = Form.useForm();
const [transport, setTransport] = useState<'stdio' | 'sse' | 'http'>(initial?.transport ?? 'stdio');
useEffect(() => {
@ -235,76 +235,50 @@ function McpForm({
}, [initial, form]);
return (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit(form.getFieldsValue());
}}>
<div className="form-item">
<label></label>
<input
{...form.getFieldProps('name')}
placeholder="例如 filesystem / playwright"
style={{ width: '100%' }}
/>
</div>
<div className="form-item">
<label>Transport</label>
<select
<Form form={form} layout="vertical" onFinish={onSubmit}>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="例如 filesystem / playwright" />
</Form.Item>
<Form.Item name="transport" label="Transport" rules={[{ required: true, message: '请选择 Transport' }]}>
<Select
value={transport}
onChange={(e) => setTransport(e.target.value as any)}
style={{ width: '100%' }}
>
<option value="stdio">stdio ()</option>
<option value="sse">SSE ()</option>
<option value="http">HTTP (Streamable)</option>
</select>
</div>
onChange={(v) => {
setTransport(v as any);
form.setFieldValue('transport', v);
}}
options={[
{ value: 'stdio', label: 'stdio (本机进程)' },
{ value: 'sse', label: 'SSE (远程)' },
{ value: 'http', label: 'HTTP (Streamable)' },
]}
/>
</Form.Item>
{transport === 'stdio' ? (
<>
<div className="form-item">
<label>Command</label>
<input
{...form.getFieldProps('command')}
placeholder="npx"
style={{ width: '100%' }}
/>
<div className="form-extra"> npx / node / python</div>
</div>
<div className="form-item">
<label>Args ()</label>
<textarea
{...form.getFieldProps('argsText')}
placeholder={'-y\n@modelcontextprotocol/server-filesystem\n/path/to/dir'}
style={{ width: '100%', minHeight: 80, fontFamily: 'Consolas, monospace', fontSize: 13 }}
/>
</div>
<div className="form-item">
<label>Env (JSON )</label>
<textarea
{...form.getFieldProps('envText')}
placeholder={'{ "API_KEY": "xxx" }'}
style={{ width: '100%', minHeight: 60, fontFamily: 'Consolas, monospace', fontSize: 13 }}
/>
</div>
<Form.Item name="command" label="Command">
<Input placeholder="npx" />
</Form.Item>
<Form.Item name="argsText" label="Args (每行一个)">
<Input.TextArea placeholder={'-y\n@modelcontextprotocol/server-filesystem\n/path/to/dir'} autoSize={{ minRows: 3, maxRows: 8 }} style={{ fontFamily: 'Consolas, monospace', fontSize: 13 }} />
</Form.Item>
<Form.Item name="envText" label="Env (JSON 对象)">
<Input.TextArea placeholder={'{ "API_KEY": "xxx" }'} autoSize={{ minRows: 2, maxRows: 8 }} style={{ fontFamily: 'Consolas, monospace', fontSize: 13 }} />
</Form.Item>
</>
) : (
<div className="form-item">
<label>URL</label>
<input
{...form.getFieldProps('url')}
placeholder="https://example.com/mcp"
style={{ width: '100%' }}
/>
</div>
<Form.Item name="url" label="URL" rules={[{ required: true, message: '请输入 URL' }]}>
<Input placeholder="https://example.com/mcp" />
</Form.Item>
)}
<Space style={{ justifyContent: 'flex-end', width: '100%', marginTop: 16 }}>
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</form>
</Form>
);
}

View File

@ -229,7 +229,7 @@ export default function MarketplacePageH5({ logic }: Props) {
</Button>
</div>
</div>
</div>
</Col>
))}
</Row>
)}

View File

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { message } from 'antd';
import { PointsMallAPI, PointsMallCategory, PointsMallOverview, PointsMallProduct, PointsMallProductsResponse } from '../../api';
import { MOCK_OVERVIEW, MOCK_PRODUCTS } from './mocks';
import type { SortKey } from './types';
import type { ExchangeFormValues, SortKey } from './types';
export function usePointsMallPageLogic() {
const [overviewLoading, setOverviewLoading] = useState(false);
@ -37,7 +37,11 @@ export function usePointsMallPageLogic() {
PointsMallAPI.promoEntries(),
]);
const me = meRes.status === 'fulfilled' ? meRes.value : { points: 0, level: 'Lv.0' };
const rawMe = meRes.status === 'fulfilled' ? meRes.value : { points: 0, level: 'Lv.0' };
const me = {
...rawMe,
points: Number((rawMe as any)?.points ?? 0),
};
const cats = categoriesRes.status === 'fulfilled' ? categoriesRes.value : MOCK_OVERVIEW.categories;
const announcements = announcementsRes.status === 'fulfilled' ? announcementsRes.value : MOCK_OVERVIEW.announcements;
const banners = bannersRes.status === 'fulfilled' ? bannersRes.value : MOCK_OVERVIEW.banners;
@ -124,10 +128,11 @@ export function usePointsMallPageLogic() {
setPendingExpiresAt(res.expiresAt || new Date(Date.now() + 30 * 60 * 1000).toISOString());
setConfirmModalVisible(false);
setExchangeModalVisible(true);
if (typeof res.remainingPoints === 'number') {
const remainingPoints = res.remainingPoints;
if (typeof remainingPoints === 'number') {
setOverview((prev) => {
if (!prev) return prev;
return { ...prev, me: { ...prev.me, points: res.remainingPoints } };
return { ...prev, me: { ...prev.me, points: remainingPoints } };
});
}
message.success('已冻结积分并预扣库存,请继续填写收件信息');
@ -140,8 +145,34 @@ export function usePointsMallPageLogic() {
}
};
const handleExchangeSubmit = async () => {
const handleExchangeSubmit = async (values: ExchangeFormValues) => {
if (!selectedProduct || !pendingOrderId) return;
setExchangeLoading(true);
try {
const res = await PointsMallAPI.exchangeSubmitShipping(pendingOrderId, values);
setExchangeModalVisible(false);
setConfirmModalVisible(false);
setPendingOrderId(null);
setPendingExpiresAt(null);
setSelectedProduct(null);
setExchangeQuantity(1);
const remainingPoints = res.remainingPoints;
if (typeof remainingPoints === 'number') {
setOverview((prev) => {
if (!prev) return prev;
return { ...prev, me: { ...prev.me, points: remainingPoints } };
});
} else {
loadOverview();
}
loadProducts();
message.success('兑换成功');
} catch (e: any) {
const msg = e?.response?.data?.message || e?.response?.data?.error || e?.message || '提交收件信息失败,请稍后重试';
message.error(msg);
} finally {
setExchangeLoading(false);
}
};
return {

View File

@ -1,15 +1,17 @@
import { Button, Card, Empty, Input, Select, Space, Spin, Tag } from 'antd';
import { Button, Card, Empty, Form, Input, Select, Space, Spin, Tag } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import type { PointsMallProduct } from '../../../api';
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
import ExchangeModal from '../components/ExchangeModal';
import ConfirmExchangeModal from '../components/ConfirmExchangeModal';
import type { ExchangeFormValues } from '../types';
interface Props {
logic: PointsMallPageLogicOutput;
}
export default function PointsMallPageH5({ logic }: Props) {
const [exchangeForm] = Form.useForm<ExchangeFormValues>();
const {
overview,
overviewLoading,
@ -246,9 +248,13 @@ export default function PointsMallPageH5({ logic }: Props) {
quantity={exchangeQuantity}
expiresAt={pendingExpiresAt}
loading={exchangeLoading}
onSubmit={logic.handleExchangeSubmit}
form={exchangeForm}
onSubmit={() => {
exchangeForm.validateFields().then((values) => logic.handleExchangeSubmit(values));
}}
onCancel={() => {
setExchangeModalVisible(false);
exchangeForm.resetFields();
logic.setPendingOrderId(null);
logic.setPendingExpiresAt(null);
logic.setSelectedProduct(null);

View File

@ -1,15 +1,17 @@
import { Button, Card, Empty, Input, Select, Space, Spin, Tag } from 'antd';
import { Button, Card, Empty, Form, Input, Select, Space, Spin, Tag } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import type { PointsMallProduct } from '../../../api';
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
import ExchangeModal from '../components/ExchangeModal';
import ConfirmExchangeModal from '../components/ConfirmExchangeModal';
import type { ExchangeFormValues } from '../types';
interface Props {
logic: PointsMallPageLogicOutput;
}
export default function PointsMallPageWeb({ logic }: Props) {
const [exchangeForm] = Form.useForm<ExchangeFormValues>();
const {
overview,
overviewLoading,
@ -239,9 +241,13 @@ export default function PointsMallPageWeb({ logic }: Props) {
quantity={exchangeQuantity}
expiresAt={pendingExpiresAt}
loading={exchangeLoading}
onSubmit={logic.handleExchangeSubmit}
form={exchangeForm}
onSubmit={() => {
exchangeForm.validateFields().then((values) => logic.handleExchangeSubmit(values));
}}
onCancel={() => {
setExchangeModalVisible(false);
exchangeForm.resetFields();
logic.setPendingOrderId(null);
logic.setPendingExpiresAt(null);
logic.setSelectedProduct(null);

View File

@ -62,8 +62,10 @@ export function useTeamsPageLogic() {
handleInvite,
handleDelete,
handleRemoveMember,
setActive,
setCreateOpen,
setInviteOpen,
setLastInviteCode,
};
}

View File

@ -7,7 +7,7 @@ import {
UserOutlined,
} from '@ant-design/icons';
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
import type { Team } from '../../../api';
import { TeamAPI, type Team } from '../../../api';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
interface Props {

View File

@ -7,7 +7,7 @@ import {
UserOutlined,
} from '@ant-design/icons';
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
import type { Team } from '../../../api';
import { TeamAPI, type Team } from '../../../api';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
interface Props {
@ -294,8 +294,7 @@ export default function TeamsPageWeb({ logic }: Props) {
}
/>
</List.Item>
);
}}
)}
/>
</Card>
) : (

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useChatPageLogic } from './chat/ChatPageLogic';
import ChatPageWeb from './chat/components/ChatPageWeb';
import ChatPageH5 from './chat/components/ChatPageH5';
import { useChatPageLogic } from './ChatPageLogic';
import ChatPageWeb from './components/ChatPageWeb';
import ChatPageH5 from './components/ChatPageH5';
const isMobileDevice = () => {
if (typeof window === 'undefined') return false;

View File

@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { App as AntApp } from 'antd';
import { SessionAPI } from '../../../api';
import type { ModelOverrides } from '../../api';
import { SessionAPI } from '../../api';
import { useChatScroll } from './hooks/useChatScroll';
import { useChatData } from './hooks/useChatData';
import { useChatSender } from './hooks/useChatSender';
import { markdownToPlainText } from './utils/copy';
const lastRoomKey = (agentId: string) => `chat:lastRoom:${agentId}`;
const isValidRoomId = (v: string | null): v is string => !!v && !String(v).startsWith('legacy_');
@ -22,6 +22,7 @@ export function useChatPageLogic() {
const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false);
const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false);
const [tplDrawerOpen, setTplDrawerOpen] = useState(false);
const [overrides, setOverrides] = useState<ModelOverrides>({});
const abortRef = useRef<AbortController | null>(null);
const { bodyRef, scrollBottom, initialScrollDoneRef } = useChatScroll();
@ -111,7 +112,7 @@ export function useChatPageLogic() {
setHighlightId,
scrollBottom,
initialScrollDoneRef,
setOverrides: (updater) => {},
setOverrides,
abort: () => abortRef.current?.abort(),
});
@ -120,16 +121,32 @@ export function useChatPageLogic() {
agent,
agentList,
roomId,
overrides: {},
overrides,
setOverrides,
messages,
setMessages,
setBranches,
loadMessages,
scrollBottom,
notify: { success: (t) => message.success(t), error: (t) => message.error(t) },
abort: abortRef,
abortRef,
});
const handleNewSession = async () => {
if (!id) return;
abortRef.current?.abort();
try {
const created = await SessionAPI.create(id);
setRoomId(created.id);
setHighlightId(null);
setMessages(() => []);
setBranches({});
message.success('已创建新会话');
} catch (e: any) {
message.error('创建房间失败:' + (e?.message ?? e));
}
};
return {
id,
roomId,
@ -147,10 +164,15 @@ export function useChatPageLogic() {
initialScrollDoneRef,
sender,
navigate,
overrides,
setOverrides,
setRoomId,
setHighlightId,
setHistoryDrawerOpen,
setMcpDrawerOpen,
setParamsDrawerOpen,
setTplDrawerOpen,
handleNewSession,
};
}

View File

@ -1,17 +1,20 @@
import { Empty } from 'antd';
import AgentSidebar from './AgentSidebar';
import ChatHeader from './components/ChatHeader';
import ChatBody from './components/ChatBody';
import ChatInput from './components/ChatInput';
import ChatDrawers from './components/ChatDrawers';
import ChatOutline from './components/ChatOutline';
import { App as AntApp, Button, Drawer, Empty, Space } from 'antd';
import { useState } from 'react';
import type { ModelOverrides } from '../../../api';
import type { ChatPageLogicOutput } from '../ChatPageLogic';
import { markdownToPlainText } from '../utils/copy';
import AgentSidebar from './AgentSidebar';
import ChatBody from './ChatBody';
import ChatDrawers from './ChatDrawers';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
import ChatOutline from './ChatOutline';
interface Props {
logic: ChatPageLogicOutput;
}
export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
const { message } = AntApp.useApp();
const [agentDrawerOpen, setAgentDrawerOpen] = useState(false);
const [outlineDrawerOpen, setOutlineDrawerOpen] = useState(false);
export default function ChatPageH5({ logic }: Props) {
const {
id,
roomId,
@ -25,32 +28,81 @@ export default function ChatPageH5({ logic }: Props) {
messages,
branches,
bodyRef,
scrollBottom,
initialScrollDoneRef,
sender,
navigate,
overrides,
setOverrides,
setRoomId,
setHighlightId,
setHistoryDrawerOpen,
setMcpDrawerOpen,
setParamsDrawerOpen,
setTplDrawerOpen,
handleNewSession,
} = logic;
return (
<div className="chat-shell h5-chat-shell">
<AgentSidebar
agentList={agentList}
activeAgentId={id}
onCreate={() => navigate('/agents/new')}
onSelect={(aid) => navigate(`/chat/${aid}`)}
/>
<Drawer
title="选择智能体"
placement="left"
open={agentDrawerOpen}
onClose={() => setAgentDrawerOpen(false)}
width={320}
styles={{ body: { padding: 0 } }}
>
<AgentSidebar
agentList={agentList}
activeAgentId={id}
onCreate={() => {
setAgentDrawerOpen(false);
navigate('/agents/new');
}}
onSelect={(aid) => {
setAgentDrawerOpen(false);
navigate(`/chat/${aid}`);
}}
/>
</Drawer>
<Drawer
title="对话大纲"
placement="right"
open={outlineDrawerOpen}
onClose={() => setOutlineDrawerOpen(false)}
width={320}
styles={{ body: { padding: 0 } }}
>
<ChatOutline
messages={messages}
activeId={highlightId}
onJump={(msgId) => {
setHighlightId(msgId);
setOutlineDrawerOpen(false);
const el = document.getElementById('msg-' + msgId);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
}}
/>
</Drawer>
<section className="chat-main h5-chat-main">
{!agent ? (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Empty description="请在左侧选择一个智能体开始对话" />
<Empty description="请选择一个智能体开始对话" />
</div>
) : (
<>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--color-border)' }}>
<Space>
<Button size="small" onClick={() => setAgentDrawerOpen(true)}>
</Button>
<Button size="small" onClick={() => setOutlineDrawerOpen(true)}>
</Button>
</Space>
</div>
<ChatHeader
agent={agent}
useStream={sender.useStream}
@ -77,20 +129,9 @@ export default function ChatPageH5({ logic }: Props) {
onSwitchBranch={sender.handleSwitchBranch}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
return navigator.clipboard
?.writeText(content)
?.then(() => message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)'));
}}
/>
/>
<ChatOutline
messages={messages}
activeId={highlightId}
onJump={(msgId) => {
setHighlightId(msgId);
const el = document.getElementById('msg-' + msgId);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
navigator.clipboard?.writeText(content).then(() => {
message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)');
});
}}
/>
</div>
@ -109,26 +150,44 @@ export default function ChatPageH5({ logic }: Props) {
onOpenTpl={() => setTplDrawerOpen(true)}
modelOptions={sender.modelOptions}
activeModelValue={sender.activeModelValue}
onChangeModel={sender.onChangeModel}
onChangeModel={(modelId) => {
const picked = sender.modelOptions.find((o) => o.value === modelId);
setOverrides((o: ModelOverrides) => ({
...o,
model_id: modelId,
model: picked?.label ?? o.model
}));
}}
agentList={agentList}
onInsertMention={() => {}}
onOpenHistory={() => setHistoryDrawerOpen(true)}
onNewSession={async () => {
if (!id) return;
abortRef.current?.abort();
try {
const created = await SessionAPI.create(id);
setRoomId(created.id);
messages.length && messages.splice(0, messages.length);
branches && Object.keys(branches).length && Object.values(branches).forEach(() => {});
} catch (e) {
message.error('创建房间失败');
}
}}
onNewSession={handleNewSession}
/>
</div>
</div>
</>
);
};
</>
)}
</section>
<ChatDrawers
agentId={id}
agent={agent}
input={sender.input}
setInput={(updater) => sender.setInput(updater)}
overrides={overrides}
setOverrides={setOverrides}
roomId={roomId}
setRoomId={setRoomId}
setHighlightId={setHighlightId}
sessionRefresh={sender.sessionRefresh}
mcpDrawerOpen={mcpDrawerOpen}
setMcpDrawerOpen={setMcpDrawerOpen}
tplDrawerOpen={tplDrawerOpen}
setTplDrawerOpen={setTplDrawerOpen}
paramsDrawerOpen={paramsDrawerOpen}
setParamsDrawerOpen={setParamsDrawerOpen}
historyDrawerOpen={historyDrawerOpen}
setHistoryDrawerOpen={setHistoryDrawerOpen}
notify={{ success: (t) => message.success(t) }}
/>
</div>
);
}

View File

@ -1,17 +1,17 @@
import { Empty } from 'antd';
import AgentSidebar from './AgentSidebar';
import ChatHeader from './components/ChatHeader';
import ChatBody from './components/ChatBody';
import ChatInput from './components/ChatInput';
import ChatDrawers from './components/ChatDrawers';
import ChatOutline from './components/ChatOutline';
import { App as AntApp, Empty } from 'antd';
import type { ChatPageLogicOutput } from '../ChatPageLogic';
import { markdownToPlainText } from '../utils/copy';
import type { ModelOverrides } from '../../../api';
import AgentSidebar from './AgentSidebar';
import ChatBody from './ChatBody';
import ChatDrawers from './ChatDrawers';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
import ChatOutline from './ChatOutline';
interface Props {
logic: ChatPageLogicOutput;
}
export default function ChatPageWeb({ logic }: { logic: ChatPageLogicOutput }) {
const { message } = AntApp.useApp();
export default function ChatPageWeb({ logic }: Props) {
const {
id,
roomId,
@ -25,14 +25,17 @@ export default function ChatPageWeb({ logic }: Props) {
messages,
branches,
bodyRef,
scrollBottom,
initialScrollDoneRef,
sender,
navigate,
overrides,
setOverrides,
setRoomId,
setHighlightId,
setHistoryDrawerOpen,
setMcpDrawerOpen,
setParamsDrawerOpen,
setTplDrawerOpen,
handleNewSession,
} = logic;
return (
@ -77,12 +80,11 @@ export default function ChatPageWeb({ logic }: Props) {
onSwitchBranch={sender.handleSwitchBranch}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
return navigator.clipboard
?.writeText(content)
?.then(() => message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)'));
navigator.clipboard?.writeText(content).then(() => {
message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)');
});
}}
/>
/>
<ChatOutline
messages={messages}
@ -109,32 +111,30 @@ export default function ChatPageWeb({ logic }: Props) {
onOpenTpl={() => setTplDrawerOpen(true)}
modelOptions={sender.modelOptions}
activeModelValue={sender.activeModelValue}
onChangeModel={sender.onChangeModel}
onChangeModel={(modelId) => {
const picked = sender.modelOptions.find((o) => o.value === modelId);
setOverrides((o: ModelOverrides) => ({
...o,
model_id: modelId,
model: picked?.label ?? o.model
}));
}}
agentList={agentList}
onInsertMention={() => {}}
onOpenHistory={() => setHistoryDrawerOpen(true)}
onNewSession={async () => {
if (!id) return;
abortRef.current?.abort();
try {
const created = await SessionAPI.create(id);
setRoomId(created.id);
messages.length && messages.splice(0, messages.length);
branches && Object.keys(branches).length && Object.values(branches).forEach(() => {});
} catch (e) {
message.error('创建房间失败');
}
}}
onNewSession={handleNewSession}
/>
</div>
</>
);
</>
)}
</section>
<ChatDrawers
agentId={id}
agent={agent}
input={sender.input}
setInput={(updater) => sender.setInput(updater(sender.input))}
setInput={(updater) => sender.setInput(updater)}
overrides={overrides}
setOverrides={setOverrides}
roomId={roomId}
setRoomId={setRoomId}
setHighlightId={setHighlightId}
@ -149,6 +149,6 @@ export default function ChatPageWeb({ logic }: Props) {
setHistoryDrawerOpen={setHistoryDrawerOpen}
notify={{ success: (t) => message.success(t) }}
/>
}
};
};
</div>
);
}

View File

@ -619,6 +619,64 @@ body {
}
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.main {
width: 100vw;
}
.chat-shell {
height: 100%;
}
.chat-header {
height: auto;
min-height: 56px;
padding: 10px 12px;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.chat-header-agent-name {
max-width: 220px;
}
.chat-header-agent-desc,
.chat-header-agent-meta {
display: none;
}
.chat-body .messages-container {
max-width: 100%;
padding: 18px 12px 96px;
}
.bubble {
max-width: 92%;
padding: 12px 14px;
font-size: 14px;
}
.chat-input-wrapper {
max-width: 100%;
padding: 0 12px 16px;
}
.chat-input-actions {
top: -22px;
right: 0;
}
.chat-model-select {
min-width: 0;
max-width: 220px;
}
}
.chat-body .messages-container {
max-width: 780px;
width: 100%;