feat: add chat page export actions

This commit is contained in:
john
2026-07-02 19:52:26 +08:00
parent 2d777ef394
commit ddfa60607a
7 changed files with 544 additions and 17 deletions
+76 -2
View File
@@ -2,6 +2,9 @@ import { ChangeEvent, useCallback, useEffect, useLayoutEffect, useRef, useState,
import { useNetworkStatus } from '../hooks/useNetworkStatus';
import { openAvatarPicker } from '../utils/userAvatar';
import { CHAT_SKILL_OPTIONS, filterChatSkills } from '../utils/chatSkills';
import { getMessageSaveActions } from '../utils/messageSave';
import { getDisplayText } from '../utils/message';
import { downloadChatMessageDocx } from '../api/client';
import {
CHAT_IMAGE_UPLOAD_MAX_COUNT,
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
@@ -10,6 +13,7 @@ import {
import { AvatarPicker } from './AvatarPicker';
import { ChatSkillPicker } from './ChatSkillPicker';
import { ChatLoadingSpinner } from './ChatLoadingSpinner';
import { ChatSharePreviewModal } from './ChatSharePreviewModal';
import { MessageList } from './MessageList';
import { PageSaveDialog } from './PageSaveDialog';
import { VoiceInputButton } from './VoiceInputButton';
@@ -76,6 +80,32 @@ function FileIcon() {
);
}
function appendLongImageDownloadParam(url: string) {
const next = new URL(url, window.location.href);
next.searchParams.set('download', 'long-image');
return next.toString();
}
function triggerUrlDownload(url: string) {
const link = document.createElement('a');
link.href = url;
link.download = '';
document.body.appendChild(link);
link.click();
link.remove();
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
}
export function ChatPanel({
variant,
user,
@@ -123,6 +153,7 @@ export function ChatPanel({
const [input, setInput] = useState('');
const [voiceRecording, setVoiceRecording] = useState(false);
const [pageSource, setPageSource] = useState<Message | null>(null);
const [sharePreviewSource, setSharePreviewSource] = useState<Message | null>(null);
const [voiceNotice, setVoiceNotice] = useState<string | null>(null);
const [pendingImages, setPendingImages] = useState<PendingChatImage[]>([]);
const [uploadingImage, setUploadingImage] = useState(false);
@@ -531,6 +562,35 @@ export function ChatPanel({
});
};
const openSaveActions = (message: Message) => {
const actions = getMessageSaveActions(getDisplayText(message), {
userId: user?.id,
username: user?.username,
});
if (actions.kind === 'page') {
setSharePreviewSource(message);
return;
}
setPageSource(message);
};
const downloadLongImage = (_message: Message, publicUrl: string) => {
triggerUrlDownload(appendLongImageDownloadParam(publicUrl));
};
const downloadDocx = async (message: Message) => {
if (!session?.id || !message.id) return;
try {
const result = await downloadChatMessageDocx({
sessionId: session.id,
messageId: message.id,
});
downloadBlob(result.blob, result.filename);
} catch (err) {
setVoiceNotice(err instanceof Error ? err.message : '文档下载失败,请重试');
}
};
return (
<>
<main
@@ -549,10 +609,11 @@ export function ChatPanel({
messages={messages}
streaming={chatState === 'streaming'}
onAvatarClick={compact ? undefined : openAvatarPicker}
onSaveAsPage={(message) => setPageSource(message)}
onSaveAsPage={openSaveActions}
onDownloadLongImage={downloadLongImage}
onDownloadDocx={(message) => void downloadDocx(message)}
publishUserId={user?.id}
publishUsername={user?.username}
sessionId={session?.id}
compact={compact}
/>
{!compact && (
@@ -587,11 +648,24 @@ export function ChatPanel({
onClose={() => setPageSource(null)}
onSaved={(result) => {
setPageSource(null);
setSharePreviewSource(null);
onPageSaved?.(result);
}}
/>
)}
{sharePreviewSource?.id && session?.id && (
<ChatSharePreviewModal
sessionId={session.id}
messageId={sharePreviewSource.id}
onClose={() => setSharePreviewSource(null)}
onSave={() => {
setPageSource(sharePreviewSource);
setSharePreviewSource(null);
}}
/>
)}
<footer className={compact ? 'space-chat-panel-footer' : `footer${showHomeWelcome ? ' footer-home' : ''}`}>
{connectStatusText && (
<div className="chat-connect-status" role="status" aria-live="polite">