fix: stabilize build and improve mobile chat layout
parent
423fc9531f
commit
faef8b6aea
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export default function MarketplacePageH5({ logic }: Props) {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -62,8 +62,10 @@ export function useTeamsPageLogic() {
|
|||
handleInvite,
|
||||
handleDelete,
|
||||
handleRemoveMember,
|
||||
setActive,
|
||||
setCreateOpen,
|
||||
setInviteOpen,
|
||||
setLastInviteCode,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
Loading…
Reference in New Issue