{agent.name}
@@ -51,9 +53,11 @@ export default function ChatHeader(props: {
流式输出
-
+ {!isMobile && (
+
+ )}
-
diff --git a/src/pages/chat/components/ChatInputH5.css b/src/pages/chat/components/ChatInputH5.css
new file mode 100644
index 0000000..aee3fbc
--- /dev/null
+++ b/src/pages/chat/components/ChatInputH5.css
@@ -0,0 +1,181 @@
+.chat-input-h5-container {
+ padding: 8px 12px calc(12px + env(safe-area-inset-bottom));
+ background: var(--color-bg);
+ width: 100%;
+ z-index: 100;
+ position: sticky;
+ bottom: 0;
+}
+
+.chat-input-h5-attachments {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.chat-input-h5-attachment-tag {
+ border-radius: 6px;
+ padding: 4px 8px;
+}
+
+.chat-input-h5-image-item {
+ position: relative;
+}
+
+.chat-input-h5-image {
+ object-fit: cover;
+ border-radius: 8px;
+ border: 1px solid var(--color-border);
+}
+
+.chat-input-h5-image-close {
+ position: absolute;
+ top: -6px;
+ right: -6px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.chat-input-h5-image-close .anticon {
+ font-size: 8px;
+}
+
+.chat-input-h5-toolbar {
+ margin-bottom: 8px;
+ overflow: hidden;
+ position: relative;
+}
+
+.chat-input-h5-toolbar-scroll {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ overflow-x: auto;
+ padding: 0 4px 4px;
+ scrollbar-width: none; /* Firefox */
+}
+
+.chat-input-h5-toolbar-scroll::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.chat-input-h5-model-select {
+ flex: 0 0 auto;
+ min-width: 100px;
+ max-width: 150px;
+}
+
+.chat-input-h5-toolbar-divider {
+ width: 1px;
+ height: 14px;
+ background: var(--color-border);
+ flex: 0 0 auto;
+ margin: 0 4px;
+}
+
+.chat-input-h5-action-icon {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 8px !important;
+ color: var(--color-text-secondary) !important;
+ font-size: 13px !important;
+}
+
+.chat-input-h5-action-icon .anticon {
+ font-size: 16px;
+}
+
+.chat-input-h5-action-icon:disabled {
+ opacity: 0.5;
+}
+
+.chat-input-h5-main {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--color-surface);
+ border-radius: 24px;
+ padding: 6px 12px 6px 18px;
+ border: 1px solid var(--color-border);
+ box-shadow: 0px 4px 20px 0px #7DB8FF4A;
+}
+
+.chat-input-h5-upload-btn {
+ padding: 4px;
+ height: auto;
+}
+
+.chat-input-h5-upload-btn .anticon {
+ font-size: 20px;
+ color: var(--color-text-secondary);
+}
+
+.chat-input-h5-textarea {
+ padding: 4px 0 !important;
+ font-size: 16px !important;
+}
+
+.chat-main .chat-input-h5-textarea:focus {
+ outline: none !important;
+ box-shadow: none !important;
+}
+
+.chat-input-h5-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.chat-input-h5-tool-btn {
+ padding: 4px;
+ height: auto;
+}
+
+.chat-input-h5-tool-btn .anticon {
+ font-size: 20px;
+ color: var(--color-text-secondary);
+}
+
+.chat-input-h5-main .ant-btn-variant-solid:disabled {
+ border: 0;
+}
+
+/* 提及选择器样式 */
+.chat-input-h5-mention-popover {
+ position: fixed;
+ z-index: 10000;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ max-height: 200px;
+ overflow-y: auto;
+ min-width: 150px;
+}
+
+.chat-input-h5-mention-empty {
+ padding: 8px;
+ color: var(--color-text-tertiary);
+}
+
+.chat-input-h5-mention-item {
+ padding: 6px 10px;
+ cursor: pointer;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.chat-input-h5-mention-item:last-child {
+ border-bottom: none;
+}
+
+.chat-input-h5-mention-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text);
+}
diff --git a/src/pages/chat/components/ChatInputH5.tsx b/src/pages/chat/components/ChatInputH5.tsx
new file mode 100644
index 0000000..2147d5b
--- /dev/null
+++ b/src/pages/chat/components/ChatInputH5.tsx
@@ -0,0 +1,303 @@
+import { ArrowUpOutlined, BookOutlined, CloseOutlined, DownOutlined, PaperClipOutlined, CameraOutlined, PlusCircleOutlined, AudioOutlined, UserOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { Button, Image as AntImage, Input, Select, Tag, Tooltip, Upload } from 'antd';
+import type { TextAreaRef } from 'antd/es/input/TextArea';
+import type { ChatAttachment } from '../../../api';
+import type { Agent } from '../../../api/agents';
+import { useState, useRef } from 'react';
+import { HistoryIcon, NewChatIcon } from '../../../components/icons';
+import './ChatInputH5.css';
+
+export default function ChatInputH5(props: {
+ input: string;
+ setInput: (v: string) => void;
+ sending: boolean;
+ attachments: ChatAttachment[];
+ setAttachments: (updater: (prev: ChatAttachment[]) => ChatAttachment[]) => void;
+ imageUrls: string[];
+ setImageUrls: (updater: (prev: string[]) => string[]) => void;
+ onSend: () => void;
+ onStop: () => void;
+ onAttach: (files: File[]) => void;
+ onOpenTpl: () => void;
+ modelOptions: Array<{ value: string; label: string }>;
+ activeModelValue: string;
+ onChangeModel: (modelId: string) => void;
+ agentList: Agent[];
+ onInsertMention?: (agentName: string) => void;
+ onOpenHistory?: () => void;
+ onNewSession?: () => void;
+ onOpenAgents?: () => void;
+ onOpenOutline?: () => void;
+ hasAgent?: boolean;
+ hasId?: boolean;
+ showActions?: boolean;
+}) {
+ const {
+ input,
+ setInput,
+ sending,
+ attachments,
+ setAttachments,
+ imageUrls,
+ setImageUrls,
+ onSend,
+ onStop,
+ onAttach,
+ onOpenTpl,
+ modelOptions,
+ activeModelValue,
+ onChangeModel,
+ agentList,
+ onInsertMention,
+ onOpenHistory,
+ onNewSession,
+ onOpenAgents,
+ onOpenOutline,
+ hasAgent = false,
+ hasId = false,
+ showActions = false
+ } = props;
+
+ const [showMentionPopover, setShowMentionPopover] = useState(false);
+ const [mentionQuery, setMentionQuery] = useState('');
+ const [mentionPos, setMentionPos] = useState<{ top: number; left: number } | null>(null);
+ const inputRef = useRef
(null);
+
+ const getCaretPos = (textarea: HTMLTextAreaElement, caretIndex: number) => {
+ const rect = textarea.getBoundingClientRect();
+ const cs = window.getComputedStyle(textarea);
+ const div = document.createElement('div');
+
+ div.style.position = 'absolute';
+ div.style.visibility = 'hidden';
+ div.style.top = '0';
+ div.style.left = '-9999px';
+ div.style.whiteSpace = 'pre-wrap';
+ div.style.wordWrap = 'break-word';
+ div.style.overflow = 'hidden';
+ div.style.boxSizing = cs.boxSizing;
+ div.style.width = rect.width + 'px';
+ div.style.fontFamily = cs.fontFamily;
+ div.style.fontSize = cs.fontSize;
+ div.style.fontWeight = cs.fontWeight;
+ div.style.fontStyle = cs.fontStyle;
+ div.style.letterSpacing = cs.letterSpacing;
+ div.style.textTransform = cs.textTransform;
+ div.style.lineHeight = cs.lineHeight;
+ div.style.padding = cs.padding;
+ div.style.border = cs.border;
+ div.style.tabSize = (cs as any).tabSize || '8';
+
+ const before = textarea.value.slice(0, caretIndex).replace(/ /g, '\u00a0');
+ div.textContent = before;
+ const span = document.createElement('span');
+ span.textContent = '\u200b';
+ div.appendChild(span);
+
+ document.body.appendChild(div);
+ const divRect = div.getBoundingClientRect();
+ const spanRect = span.getBoundingClientRect();
+ document.body.removeChild(div);
+
+ const lineHeight = Number.parseFloat(cs.lineHeight) || Number.parseFloat(cs.fontSize) * 1.2;
+ return {
+ top: rect.top + (spanRect.top - divRect.top) - textarea.scrollTop,
+ left: rect.left + (spanRect.left - divRect.left) - textarea.scrollLeft,
+ lineHeight
+ };
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setInput(value);
+
+ const cursorPos = e.target.selectionStart ?? value.length;
+ const textBeforeCursor = value.slice(0, cursorPos);
+ const atIndex = textBeforeCursor.lastIndexOf('@');
+
+ if (atIndex !== -1 && (atIndex === 0 || /\s$/.test(textBeforeCursor.slice(0, atIndex)))) {
+ const query = textBeforeCursor.slice(atIndex + 1);
+ if (!query.includes(' ')) {
+ setMentionQuery(query);
+ requestAnimationFrame(() => {
+ const textarea = inputRef.current?.resizableTextArea?.textArea;
+ if (!textarea) return;
+ const caret = getCaretPos(textarea, cursorPos);
+ const top = Math.min(window.innerHeight - 8, caret.top + caret.lineHeight + 8);
+ const left = Math.min(window.innerWidth - 160, Math.max(8, caret.left));
+ setMentionPos({ top, left });
+ setShowMentionPopover(true);
+ });
+ return;
+ }
+ }
+ setShowMentionPopover(false);
+ };
+
+ const filteredAgents = agentList.filter(a =>
+ a.name.toLowerCase().includes(mentionQuery.toLowerCase())
+ );
+
+ const handleSelectAgent = (agent: Agent) => {
+ const textarea = inputRef.current?.resizableTextArea?.textArea;
+ if (!textarea) return;
+
+ const value = input;
+ const cursorPos = textarea.selectionStart ?? value.length;
+ const textBefore = value.slice(0, cursorPos);
+ const atIndex = textBefore.lastIndexOf('@');
+
+ if (atIndex === -1) {
+ setShowMentionPopover(false);
+ return;
+ }
+
+ const newValue = value.slice(0, atIndex) + `@${agent.name} ` + value.slice(cursorPos);
+ setInput(newValue);
+ setShowMentionPopover(false);
+
+ requestAnimationFrame(() => {
+ textarea.focus();
+ const newCursor = atIndex + agent.name.length + 2;
+ textarea.setSelectionRange(newCursor, newCursor);
+ });
+ };
+
+ return (
+
+ {/* 顶部附件展示 */}
+
+ {attachments.map((a, i) => (
+
setAttachments((arr) => arr.filter((_, j) => j !== i))}>
+ 📎 {a.name}
+
+ ))}
+ {imageUrls.map((u, i) => (
+
+
+
}
+ className="chat-input-h5-image-close"
+ onClick={() => setImageUrls((arr) => arr.filter((_, j) => j !== i))}
+ />
+
+ ))}
+
+
+ {/* 模型切换与工具栏 */}
+
+
+
}
+ placeholder="选择模型"
+ onChange={onChangeModel}
+ dropdownStyle={{ minWidth: 160 }}
+ />
+
+
} onClick={onOpenAgents}>
+ 智能体
+
+
} onClick={onOpenOutline}>
+ 大纲
+
+
} onClick={onOpenHistory}>
+ 历史
+
+
} onClick={onNewSession}>
+ 新建
+
+
} onClick={onOpenTpl}>
+ 模板
+
+
+
+
+ {/* 主输入框区域 */}
+
+
{
+ if (e.key !== 'Enter') return;
+ if ((e as any).isComposing) return;
+ if (!e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
+ e.preventDefault();
+ onSend();
+ }
+ }}
+ disabled={sending}
+ />
+
+
+ {sending ? (
+ } />
+ ) : (
+ <>
+ {
+ onAttach(files as File[]);
+ return false;
+ }}
+ showUploadList={false}
+ accept=".txt,.md,.markdown,.json,.csv,.pdf,.docx,.html,.htm,image/png,image/jpeg,image/webp,image/gif"
+ >
+ } className="chat-input-h5-upload-btn" />
+
+ }
+ disabled={!input.trim()}
+ />
+ >
+ )}
+
+
+
+ {/* 提及选择器 */}
+ {showMentionPopover && mentionPos && (
+
+ {filteredAgents.length === 0 ? (
+
+ 未找到匹配的智能体
+
+ ) : (
+ filteredAgents.map(agent => (
+
handleSelectAgent(agent)}
+ >
+
+ {agent.name}
+
+
+ ))
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/pages/chat/components/ChatPageH5.css b/src/pages/chat/components/ChatPageH5.css
new file mode 100644
index 0000000..c788ba9
--- /dev/null
+++ b/src/pages/chat/components/ChatPageH5.css
@@ -0,0 +1,21 @@
+@media (max-width: 768px) {
+ .h5-chat-shell {
+ /* 移除固定高度,允许跟随全局滚动容器 */
+ display: flex;
+ flex-direction: column;
+ }
+
+ .h5-chat-main {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .h5-chat-content-row {
+ /* 移除内部滚动,改为由 App 的 main-content 统一处理 */
+ min-height: 0;
+ }
+
+ .h5-chat-content-row .chat-body .messages-container {
+ padding-bottom: 2rem;
+ }
+}
diff --git a/src/pages/chat/components/ChatPageH5.tsx b/src/pages/chat/components/ChatPageH5.tsx
index aabbc9f..ad62416 100644
--- a/src/pages/chat/components/ChatPageH5.tsx
+++ b/src/pages/chat/components/ChatPageH5.tsx
@@ -7,8 +7,9 @@ import AgentSidebar from './AgentSidebar';
import ChatBody from './ChatBody';
import ChatDrawers from './ChatDrawers';
import ChatHeader from './ChatHeader';
-import ChatInput from './ChatInput';
+import ChatInputH5 from './ChatInputH5';
import ChatOutline from './ChatOutline';
+import './ChatPageH5.css';
export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
const { message } = AntApp.useApp();
@@ -86,23 +87,6 @@ export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
-
-
- setAgentDrawerOpen(true)}>
- 智能体
-
- setOutlineDrawerOpen(true)}>
- 大纲
-
- setHistoryDrawerOpen(true)}>
- 历史对话
-
-
- 新建会话
-
-
-
-
{!agent ? (
@@ -122,6 +106,7 @@ export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
-
-
setTplDrawerOpen(true)}
- modelOptions={sender.modelOptions}
- activeModelValue={sender.activeModelValue}
- 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={handleNewSession}
- showActions={false}
- />
>
)}
+ setTplDrawerOpen(true)}
+ modelOptions={sender.modelOptions}
+ activeModelValue={sender.activeModelValue}
+ onChangeModel={(modelId: string) => {
+ 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={handleNewSession}
+ onOpenAgents={() => setAgentDrawerOpen(true)}
+ onOpenOutline={() => setOutlineDrawerOpen(true)}
+ hasAgent={!!agent}
+ hasId={!!id}
+ showActions={false}
+ />
void;
onSwitchBranch?: (userMsgId: string, branchId: string) => void;
onCopy?: (text: string, mode: CopyMode) => void;
+ isMobile?: boolean;
}) {
- const { message, agentList, currentAgentId, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy } = props;
+ const { message, agentList, currentAgentId, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy, isMobile } = props;
const speakerType = (message as any)?.speaker?.type as ('user' | 'agent' | undefined);
const speakerId = (message as any)?.speaker?.id as string | undefined;
@@ -46,78 +48,59 @@ export default function MessageItem(props: {
return (
{!isUser ? (
-
+
{answerAgent?.name?.charAt(0)?.toUpperCase() || 'A'}
-
-
-
+
+
+
{answerAgent?.name || 'AI'}
{message.content}
-
-
- {hasBranches && (
-
-
- ‹
-
-
- {activeIdx + 1} / {total}
-
-
- ›
-
-
- )}
- {message.meta?.aborted &&
已停止}
-
onCopy?.(message.content, 'plain') },
- { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
- ]
- }}
- >
-
-
-
-
+
+ {hasBranches && (
+
+
+ ‹
+
+
+ {activeIdx + 1} / {total}
+
+
+ ›
+
+
+ )}
+ {message.meta?.aborted && 已停止}
+ onCopy?.(message.content, 'plain') },
+ { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
+ ]
+ }}
+ >
+
+ } />
+
+
+
+ } disabled={busy} onClick={() => onRegenerate?.(message.id)} />
-
-
- onRegenerate?.(message.id)}>
- 🔄
-
-
+
- {message.meta && (
+ {!isMobile && message.meta && (
{!!message.meta.reasoning && }
{!!message.meta.retrieved?.length && }
@@ -127,8 +110,8 @@ export default function MessageItem(props: {
) : (
-
-
+
+
{message.content.includes(' ? (
{message.content}
@@ -139,7 +122,7 @@ export default function MessageItem(props: {
)}
-
我
+
我
)}
diff --git a/src/styles.css b/src/styles.css
index 314a25c..3cab66d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -273,8 +273,19 @@ body {
.main {
flex: 1;
- overflow: auto;
background: var(--color-bg);
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.main-content {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ min-height: 0;
+ -webkit-overflow-scrolling: touch;
}
.main-chat {
@@ -536,6 +547,10 @@ body {
margin-right: 12px;
}
+.is-h5 .chat-header-stream-toggle {
+ margin-right: 4px;;
+}
+
.chat-header-stream-label {
font-size: 12px;
color: var(--color-text-secondary);
@@ -619,6 +634,7 @@ body {
.main {
width: 100vw;
+ height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
@@ -631,7 +647,6 @@ body {
align-items: center;
gap: 0.5rem;
padding: 0 0.5rem;
- border-bottom: 1px solid var(--color-border);
background: var(--color-surface);
}
@@ -647,8 +662,10 @@ body {
.main-content {
flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
min-height: 0;
- overflow: auto;
+ -webkit-overflow-scrolling: touch;
}
.chat-shell {
@@ -680,12 +697,6 @@ body {
padding: 1.125rem 0.75rem 6rem;
}
- .bubble {
- max-width: 92%;
- padding: 0.75rem 0.875rem;
- font-size: 0.875rem;
- }
-
.chat-input-wrapper {
max-width: 100%;
padding: 0 0.75rem 1rem;
@@ -743,15 +754,18 @@ body {
.bubble {
max-width: 78%;
display: inline-block;
- padding: 14px 18px;
+ padding: 8px;
border-radius: 14px;
- margin-bottom: 14px;
white-space: pre-wrap;
word-wrap: break-word;
- line-height: 1.65;
+ line-height: 1.5;
font-size: 14.5px;
}
+.is-h5 .bubble {
+ max-width: 94%;
+}
+
.bubble.user {
background: #0a84ff;
color: #ffffff;
@@ -760,7 +774,7 @@ body {
}
.bubble.assistant {
- background: #e9e9eb;
+ background: #edf1f6;
color: #111827;
border: 0;
border-bottom-left-radius: 5px;
@@ -815,20 +829,6 @@ body {
margin: 0.2em 0;
}
-.chat-copy-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- cursor: pointer;
- color: var(--color-text-secondary);
-}
-
-.chat-copy-icon:hover {
- color: var(--color-text);
-}
-
.bubble.assistant table {
border-collapse: collapse;
width: 100%;
diff --git a/src/styles/app-shell-h5.css b/src/styles/app-shell-h5.css
index e973f46..1a353a2 100644
--- a/src/styles/app-shell-h5.css
+++ b/src/styles/app-shell-h5.css
@@ -5,7 +5,6 @@
align-items: center;
gap: 0.5rem;
padding: 0 0.625rem;
- border-bottom: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(1.125rem);
}