feat(ChatExcel): Better Chat Excel (#2423)

This commit is contained in:
Fangyin Cheng
2025-03-10 11:37:32 +08:00
committed by GitHub
parent 75c1454938
commit 93eb3a786c
202 changed files with 1862 additions and 751 deletions

View File

@@ -1,4 +1,5 @@
import ChatHeader from '@/new-components/chat/header/ChatHeader';
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons';
import dynamic from 'next/dynamic';
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
@@ -8,34 +9,103 @@ const ChatCompletion = dynamic(() => import('@/new-components/chat/content/ChatC
const ChatContentContainer = ({}, ref: React.ForwardedRef<any>) => {
const scrollRef = useRef<HTMLDivElement>(null);
const [isScrollToTop, setIsScrollToTop] = useState<boolean>(false);
const [showScrollButtons, setShowScrollButtons] = useState<boolean>(false);
const [isAtTop, setIsAtTop] = useState<boolean>(true);
const [isAtBottom, setIsAtBottom] = useState<boolean>(false);
useImperativeHandle(ref, () => {
return scrollRef.current;
});
const handleScroll = () => {
if (!scrollRef.current) return;
const container = scrollRef.current;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
const buffer = 20; // Small buffer for better UX
// Check if we're at the top
setIsAtTop(scrollTop <= buffer);
// Check if we're at the bottom
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - buffer);
// Header visibility
if (scrollTop >= 42 + 32) {
setIsScrollToTop(true);
} else {
setIsScrollToTop(false);
}
// Show scroll buttons when content is scrollable
const isScrollable = scrollHeight > clientHeight;
setShowScrollButtons(isScrollable);
};
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.addEventListener('scroll', () => {
const st = scrollRef.current?.scrollTop || 0;
if (st >= 42 + 32) {
setIsScrollToTop(true);
} else {
setIsScrollToTop(false);
}
});
scrollRef.current.addEventListener('scroll', handleScroll);
// Check initially if content is scrollable
const isScrollable = scrollRef.current.scrollHeight > scrollRef.current.clientHeight;
setShowScrollButtons(isScrollable);
}
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
scrollRef.current && scrollRef.current.removeEventListener('scroll', () => {});
scrollRef.current && scrollRef.current.removeEventListener('scroll', handleScroll);
};
}, []);
const scrollToTop = () => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: 0,
behavior: 'smooth',
});
}
};
const scrollToBottom = () => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: 'smooth',
});
}
};
return (
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 overflow-hidden relative'>
<div ref={scrollRef} className='h-full w-full mx-auto overflow-y-auto'>
<ChatHeader isScrollToTop={isScrollToTop} />
<ChatCompletion />
</div>
{showScrollButtons && (
<div className='absolute right-6 bottom-24 flex flex-col gap-2'>
{!isAtTop && (
<button
onClick={scrollToTop}
className='w-10 h-10 bg-white dark:bg-[rgba(255,255,255,0.2)] border border-gray-200 dark:border-[rgba(255,255,255,0.2)] rounded-full flex items-center justify-center shadow-md hover:shadow-lg transition-shadow'
aria-label='Scroll to top'
>
<VerticalAlignTopOutlined className='text-[#525964] dark:text-[rgba(255,255,255,0.85)]' />
</button>
)}
{!isAtBottom && (
<button
onClick={scrollToBottom}
className='w-10 h-10 bg-white dark:bg-[rgba(255,255,255,0.2)] border border-gray-200 dark:border-[rgba(255,255,255,0.2)] rounded-full flex items-center justify-center shadow-md hover:shadow-lg transition-shadow'
aria-label='Scroll to bottom'
>
<VerticalAlignBottomOutlined className='text-[#525964] dark:text-[rgba(255,255,255,0.85)]' />
</button>
)}
</div>
)}
</div>
);
};

View File

@@ -56,12 +56,12 @@ const ChatCompletion: React.FC = () => {
if (res) {
const paramKey: string[] = res?.param_need?.map(i => i.type) || [];
const resModel = res?.param_need?.filter(item => item.type === 'model')[0]?.value || model;
const temperature = res?.param_need?.filter(item => item.type === 'temperature')[0]?.value || 0.5;
const maxNewTokens = res?.param_need?.filter(item => item.type === 'max_new_tokens')[0]?.value || 2048;
const temperature = res?.param_need?.filter(item => item.type === 'temperature')[0]?.value || 0.6;
const maxNewTokens = res?.param_need?.filter(item => item.type === 'max_new_tokens')[0]?.value || 4000;
const resource = res?.param_need?.filter(item => item.type === 'resource')[0]?.bind_value;
setAppInfo(res || ({} as IApp));
setTemperatureValue(temperature || 0.5);
setMaxNewTokensValue(maxNewTokens || 2048);
setTemperatureValue(temperature || 0.6);
setMaxNewTokensValue(maxNewTokens || 4000);
setModelValue(resModel);
setResourceValue(resource);
await handleChat(initMessage.message, {

View File

@@ -1,8 +1,16 @@
import markdownComponents from '@/components/chat/chat-content/config';
import { IChatDialogueMessageSchema } from '@/types/chat';
import { STORAGE_USERINFO_KEY } from '@/utils/constants/index';
import { CheckOutlined, ClockCircleOutlined, CloseOutlined, CodeOutlined, LoadingOutlined } from '@ant-design/icons';
import {
CheckOutlined,
ClockCircleOutlined,
CloseOutlined,
CodeOutlined,
CopyOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { GPTVis } from '@antv/gpt-vis';
import { message } from 'antd';
import classNames from 'classnames';
import Image from 'next/image';
import { useSearchParams } from 'next/navigation';
@@ -173,8 +181,36 @@ const ChatContent: React.FC<{
<div className={`flex ${scene === 'chat_agent' && !thinking ? 'flex-1' : ''} overflow-hidden`}>
{/* 用户提问 */}
{!isRobot && (
<div className='flex flex-1 items-center text-sm text-[#1c2533] dark:text-white'>
{typeof context === 'string' && context}
<div className='flex flex-1 relative group'>
<div
className='flex-1 text-sm text-[#1c2533] dark:text-white'
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
>
{typeof context === 'string' && context}
</div>
{typeof context === 'string' && context.trim() && (
<div className='absolute right-0 top-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200'>
<button
className='flex items-center justify-center w-8 h-8 text-[#525964] dark:text-[rgba(255,255,255,0.6)] hover:text-[#1677ff] dark:hover:text-white transition-colors'
onClick={() => {
if (typeof context === 'string') {
navigator.clipboard
.writeText(context)
.then(() => {
message.success(t('copy_to_clipboard_success'));
})
.catch(err => {
console.error(t('copy_to_clipboard_failed'), err);
message.error(t('copy_to_clipboard_failed'));
});
}
}}
title={t('copy_to_clipboard')}
>
<CopyOutlined />
</button>
</div>
)}
</div>
)}
{/* ai回答 */}

View File

@@ -52,7 +52,7 @@ const MaxNewTokens: React.FC<{
max={20480}
step={1}
onChange={handleSliderChange}
value={typeof maxNewTokensValue === 'number' ? maxNewTokensValue : 2048}
value={typeof maxNewTokensValue === 'number' ? maxNewTokensValue : 4000}
/>
<InputNumber
size='small'

View File

@@ -21,6 +21,7 @@ const Resource: React.FC<{
const { setResourceValue, appInfo, refreshHistory, refreshDialogList, modelValue, resourceValue } =
useContext(ChatContentContext);
const { temperatureValue, maxNewTokensValue } = useContext(ChatContentContext);
const searchParams = useSearchParams();
const scene = searchParams?.get('scene') ?? '';
const chatId = searchParams?.get('id') ?? '';
@@ -97,6 +98,8 @@ const Resource: React.FC<{
chatMode: scene,
data: formData,
model: modelValue,
temperatureValue,
maxNewTokensValue,
config: {
timeout: 1000 * 60 * 60,
},