Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GPTLink Web</title>
</head>
<body>
<body class="h-screen overflow-hidden">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@radix-ui/react-tooltip": "^1.0.6",
"@types/js-cookie": "^3.0.3",
"dayjs": "^1.11.7",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
"i18next": "^22.5.0",
"lodash-es": "^4.17.21",
Expand All @@ -52,6 +53,7 @@
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@hookform/resolvers": "^3.1.0",
"@types/file-saver": "^2.0.5",
"@types/lodash-es": "^4.17.7",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
Expand Down
20 changes: 19 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface BillType {
name: string;
num: number;
used: number;
expired_at?: string;
}

export interface UserPackageType {
Expand Down
2 changes: 0 additions & 2 deletions src/hooks/use-app-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect } from 'react';
import { isEmpty } from 'lodash-es';

import appService from '@/api/app';
import { useAppStore } from '@/store';
Expand All @@ -9,7 +8,6 @@ const useAppConfig = () => {

useEffect(() => {
const getAppConfig = async () => {
if (!isEmpty(appConfig)) return;
const res = await appService.getAppConfig();
setAppConfig(res);
};
Expand Down
2 changes: 1 addition & 1 deletion src/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const App = () => {
useAppConfig();

return (
<div className="flex h-screen flex-col ">
<div className="flex h-screen flex-col">
<Header />
<Outlet />
</div>
Expand Down
1 change: 1 addition & 0 deletions src/pages/billing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function Billing() {
toast.success('支付成功');
setPayDialogShow(false);
setPayInfo(null);
clearInterval(payStatusInterval);
}
}, 1500);
};
Expand Down
207 changes: 158 additions & 49 deletions src/pages/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import { useState, useRef, useEffect } from 'react';
import { Loader2, PauseOctagon, SendIcon, Trash2Icon } from 'lucide-react';
import { useState, useRef, useEffect, useMemo } from 'react';
import { Loader2, PauseOctagon, SendIcon, Trash2Icon, DownloadIcon } from 'lucide-react';

import { useChatStore } from '@/store';
import { useChatStore, useUserStore } from '@/store';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import IconSvg from '@/components/Icon';
import { Checkbox } from '@/components/ui/checkbox';
import MessageExporter from './MessageExporter';

import { ChatItem } from './ChatItem';
import classNames from 'classnames';

let scrollIntoViewTimeId = -1;
const Footer = () => {

const Footer = ({
isDownload = false,
selectedMessagesIDs,
onIsDownloadChange,
onSelectMessagesIds,
}: {
isDownload: boolean;
selectedMessagesIDs: string[];
onIsDownloadChange: (val: boolean) => void;
onSelectMessagesIds: (ids: string[]) => void;
}) => {
const [userInput, setUserInput] = useState('');
const [sendUserMessage, isStream, clearCurrentConversation, stopStream] = useChatStore((state) => [
const [sendUserMessage, isStream, clearCurrentConversation, stopStream, currentChatData] = useChatStore((state) => [
state.sendUserMessage,
state.isStream,
state.clearCurrentConversation,
state.stopStream,
state.currentChatData(),
]);
const [{ openid }] = useUserStore((state) => [state.userInfo]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.code === 'Enter' && !e.shiftKey && userInput.replace(/\n/g, '')) {
Expand All @@ -36,6 +51,16 @@ const Footer = () => {

const textAreaRef = useRef<HTMLTextAreaElement>(null);

const exportMessages = useMemo(() => {
return currentChatData.filter((item) => {
return selectedMessagesIDs.includes(item.id);
});
}, [onSelectMessagesIds]);

const allMessagesIds = useMemo(() => {
return currentChatData.map((item) => item.id);
}, [currentChatData]);

useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.style.height = '0px';
Expand All @@ -45,53 +70,108 @@ const Footer = () => {
}, [textAreaRef, userInput]);

return (
<footer className="flex items-end gap-4 p-4">
<Button
variant={'ghost'}
className="mb-1 h-9 w-9 shrink-0 rounded-full p-0"
disabled={isStream}
onClick={() => {
if (confirm('你确定要清除所有的消息吗?')) {
clearCurrentConversation();
setUserInput('');
}
}}
>
<Trash2Icon size={16} />
</Button>
<div className="relative flex-1">
{isStream && (
<div className="absolute left-0 z-10 flex w-full justify-center">
<Button variant="destructive" onClick={() => stopStream()}>
<PauseOctagon></PauseOctagon>
<footer
className={classNames('flex items-end gap-4 p-4', {
'border-t items-center justify-between': isDownload,
})}
>
{!isDownload ? (
<>
<div className="flex gap-1">
<Button
variant={'ghost'}
className="mb-1 h-9 w-9 shrink-0 rounded-full p-0"
disabled={isStream}
onClick={() => {
if (confirm('你确定要清除所有的消息吗?')) {
clearCurrentConversation();
setUserInput('');
}
}}
>
<Trash2Icon size={16} />
</Button>
<Button
variant={'ghost'}
className="mb-1 h-9 w-9 shrink-0 rounded-full p-0"
disabled={isStream}
onClick={() => {
onIsDownloadChange(true);
}}
>
<DownloadIcon size={16} />
</Button>
</div>
)}
<Textarea
ref={textAreaRef}
className={classNames('h-10 max-h-[7rem] min-h-[40px] w-full flex-1 resize-none scroll-bar-none', {
'blur-sm': isStream,
})}
onKeyDown={handleKeyDown}
disabled={isStream}
value={userInput}
placeholder="来说点什么...(Shift + Enter = 换行)"
onChange={(val) => setUserInput(val.target.value)}
/>
</div>
<Button
disabled={isStream || !userInput.replace(/\n/g, '')}
onClick={() => {
handleSendUserMessage();
}}
>
{!isStream ? <SendIcon /> : <Loader2 className="m-auto my-32 animate-spin" />}
</Button>
<div className="relative flex-1">
{isStream && (
<div className="absolute left-0 z-10 flex w-full justify-center">
<Button variant="destructive" onClick={() => stopStream()}>
<PauseOctagon />
</Button>
</div>
)}
<Textarea
ref={textAreaRef}
className={classNames('h-10 max-h-[7rem] min-h-[40px] w-full flex-1 resize-none scroll-bar-none', {
'blur-sm': isStream,
})}
onKeyDown={handleKeyDown}
disabled={isStream}
value={userInput}
placeholder="来说点什么...(Shift + Enter = 换行)"
onChange={(val) => setUserInput(val.target.value)}
/>
</div>
<Button
disabled={isStream || !userInput.replace(/\n/g, '')}
onClick={() => {
handleSendUserMessage();
}}
>
{!isStream ? <SendIcon /> : <Loader2 className="m-auto my-32 animate-spin" />}
</Button>
</>
) : (
<>
<div className="flex items-center">
<Checkbox
checked={selectedMessagesIDs.length === allMessagesIds.length}
className="mr-2"
onCheckedChange={(val) => {
if (val) {
console.log(allMessagesIds);
onSelectMessagesIds(allMessagesIds);
} else {
onSelectMessagesIds([]);
}
}}
/>
全选
</div>
<MessageExporter messages={exportMessages} shareUrl={location.origin + `/chat?shareOpenId=${openid}`} />
<Button
variant={'destructive'}
onClick={() => {
onIsDownloadChange(false);
}}
>
取消
</Button>
</>
)}
</footer>
);
};

const ChatBody = () => {
const ChatBody = ({
isDownload,
selectedMessagesIDs,
onSelectMessagesIds,
}: {
isDownload: boolean;
selectedMessagesIDs: string[];
onSelectMessagesIds: (ids: string[]) => void;
}) => {
const [isStream, currentChatData] = useChatStore((state) => [state.isStream, state.currentChatData()]);

const bottom = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -126,18 +206,47 @@ const ChatBody = () => {
</div>
)}
{currentChatData.map((item, index) => (
<ChatItem key={index} data={item} />
<ChatItem
key={index}
data={item}
isCheckedMode={isDownload}
isChecked={selectedMessagesIDs.includes(item.id || '')}
onCheckedChange={(val) => {
if (val) {
onSelectMessagesIds(selectedMessagesIDs.concat(item.id));
} else {
onSelectMessagesIds(selectedMessagesIDs.filter((id) => id !== item.id));
}
}}
/>
))}
</main>
</ScrollArea>
);
};

const Chat = () => {
const [isDownload, setIsDownload] = useState(false);
const [selectedMessagesIDs, setSelectedMessagesIDs] = useState<string[]>([]);

useEffect(() => {
if (!isDownload) {
setSelectedMessagesIDs([]);
}
}, [isDownload]);
return (
<div className="flex h-full w-full flex-col overflow-hidden">
<ChatBody />
<Footer />
<ChatBody
isDownload={isDownload}
selectedMessagesIDs={selectedMessagesIDs}
onSelectMessagesIds={(ids: string[]) => setSelectedMessagesIDs(ids)}
/>
<Footer
selectedMessagesIDs={selectedMessagesIDs}
isDownload={isDownload}
onIsDownloadChange={(val) => setIsDownload(val)}
onSelectMessagesIds={(ids: string[]) => setSelectedMessagesIDs(ids)}
/>
</div>
);
};
Expand Down
Loading