mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-25 18:26:48 +08:00
22
This commit is contained in:
@@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) {
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,7 @@
|
||||
.icon-button-text {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ChatList() {
|
||||
index={i}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => selectSession(i)}
|
||||
onDelete={chatStore.deleteSession}
|
||||
onDelete={() => chatStore.deleteSession(i)}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 480px;
|
||||
max-width: 900px;
|
||||
max-width: 1200px;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
@@ -61,6 +61,27 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
transition: width ease 0.1s;
|
||||
}
|
||||
|
||||
.sidebar-drag {
|
||||
$width: 10px;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: $width;
|
||||
background-color: var(--black);
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
transition: all ease 0.3s;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.window-content {
|
||||
@@ -228,9 +249,12 @@
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.chat-item-count {}
|
||||
|
||||
.chat-item-date {}
|
||||
.chat-item-count,
|
||||
.chat-item-date {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-tail {
|
||||
display: flex;
|
||||
@@ -552,6 +576,7 @@
|
||||
|
||||
.export-content {
|
||||
white-space: break-spaces;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
|
||||
require("../polyfill");
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
MouseEventHandler,
|
||||
} from "react";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
@@ -26,6 +32,7 @@ import { Chat } from "./chat";
|
||||
import dynamic from "next/dynamic";
|
||||
import { REPO_URL } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@@ -78,6 +85,53 @@ function useSwitchTheme() {
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(500, Math.max(220, x));
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 100) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobileScreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
|
||||
);
|
||||
}, [chatStore.config.sidebarWidth]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
};
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||
|
||||
@@ -104,6 +158,9 @@ function _Home() {
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown } = useDragSideBar();
|
||||
|
||||
useSwitchTheme();
|
||||
|
||||
if (loading) {
|
||||
@@ -190,6 +247,11 @@ function _Home() {
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className={styles["window-content"]}>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function Markdown(props: { content: string }) {
|
||||
components={{
|
||||
pre: PreCode,
|
||||
}}
|
||||
linkTarget={'_blank'}
|
||||
linkTarget={"_blank"}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
.password-input-container {
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.password-eye {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
min-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
.popover {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
@@ -126,6 +127,7 @@
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
|
||||
.toast-content {
|
||||
max-width: 80vw;
|
||||
@@ -140,6 +142,7 @@
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: all;
|
||||
|
||||
.toast-action {
|
||||
padding-left: 20px;
|
||||
|
||||
41
app/icons/max.svg
Normal file
41
app/icons/max.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||
height="16" viewBox="0 0 16 16" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path id="路径 1"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"
|
||||
d="M0,0L3.33,3.3 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
|
||||
d="M0,3.3L3.33,0 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
|
||||
d="M3.3,3.3L0,0 " />
|
||||
<path id="路径 4"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
|
||||
d="M3.3,0L0,3.3 " />
|
||||
<path id="路径 5"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(11 2) rotate(0 1.5 1.5)" d="M0,0L3,0L3,3 " />
|
||||
<path id="路径 6"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(11 11) rotate(0 1.5 1.5)" d="M3,0L3,3L0,3 " />
|
||||
<path id="路径 7"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 11) rotate(0 1.5 1.5)" d="M3,3L0,3L0,0 " />
|
||||
<path id="路径 8"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 2) rotate(0 1.5 1.5)" d="M0,3L0,0L3,0 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
45
app/icons/min.svg
Normal file
45
app/icons/min.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||
height="16" viewBox="0 0 16 16" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path id="路径 1"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"
|
||||
d="M0,0L3.33,3.3 " />
|
||||
<path id="路径 2"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
|
||||
d="M0,3.3L3.33,0 " />
|
||||
<path id="路径 3"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
|
||||
d="M3.3,3.3L0,0 " />
|
||||
<path id="路径 4"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
|
||||
d="M3.3,0L0,3.3 " />
|
||||
<path id="路径 5"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.666666666666666 2.333333333333333) rotate(0 1.5 1.5)"
|
||||
d="M0,0L0,3L3,3 " />
|
||||
<path id="路径 6"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2.333333333333333 2.333333333333333) rotate(0 1.5 1.5)"
|
||||
d="M3,0L3,3L0,3 " />
|
||||
<path id="路径 7"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2.333333333333333 10.666666666666666) rotate(0 1.5 1.5)"
|
||||
d="M3,3L3,0L0,0 " />
|
||||
<path id="路径 8"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(10.666666666666666 10.666666666666666) rotate(0 1.4832500000000004 1.5)"
|
||||
d="M0,3L0,0L2.97,0 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
17
app/icons/share.svg
Normal file
17
app/icons/share.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||
height="16" viewBox="0 0 16 16" fill="none">
|
||||
<defs>
|
||||
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||
<mask id="bg-mask-0" fill="white">
|
||||
<use xlink:href="#path_0"></use>
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path id="路径 1"
|
||||
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||
transform="translate(2 1.3333333333333333) rotate(0 6.333333333333333 6.5)"
|
||||
d="M6.67,3.67C1.67,3.67 0,7.33 0,13C0,13 2,8 6.67,8L6.67,11.67L12.67,6L6.67,0L6.67,3.67Z " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 740 B |
@@ -9,7 +9,7 @@ const makeRequestParam = (
|
||||
options?: {
|
||||
filterBot?: boolean;
|
||||
stream?: boolean;
|
||||
}
|
||||
},
|
||||
): ChatRequest => {
|
||||
let sendMessages = messages.map((v) => ({
|
||||
role: v.role,
|
||||
@@ -20,7 +20,11 @@ const makeRequestParam = (
|
||||
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
|
||||
}
|
||||
|
||||
const modelConfig = useChatStore.getState().config.modelConfig;
|
||||
const modelConfig = { ...useChatStore.getState().config.modelConfig };
|
||||
|
||||
// @yidadaa: wont send max_tokens, because it is nonsense for Muggles
|
||||
// @ts-expect-error
|
||||
delete modelConfig.max_tokens;
|
||||
|
||||
return {
|
||||
messages: sendMessages,
|
||||
@@ -84,7 +88,7 @@ export async function requestUsage() {
|
||||
|
||||
const [used, subs] = await Promise.all([
|
||||
requestOpenaiClient(
|
||||
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
|
||||
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
|
||||
)(null, "GET"),
|
||||
requestOpenaiClient("dashboard/billing/subscription")(null, "GET"),
|
||||
]);
|
||||
@@ -124,7 +128,7 @@ export async function requestChatStream(
|
||||
onMessage: (message: string, done: boolean) => void;
|
||||
onError: (error: Error, statusCode?: number) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
}
|
||||
},
|
||||
) {
|
||||
const req = makeRequestParam(messages, {
|
||||
stream: true,
|
||||
@@ -213,7 +217,7 @@ export const ControllerPool = {
|
||||
addController(
|
||||
sessionIndex: number,
|
||||
messageId: number,
|
||||
controller: AbortController
|
||||
controller: AbortController,
|
||||
) {
|
||||
const key = this.key(sessionIndex, messageId);
|
||||
this.controllers[key] = controller;
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface ChatConfig {
|
||||
theme: Theme;
|
||||
tightBorder: boolean;
|
||||
sendPreviewBubble: boolean;
|
||||
sidebarWidth: number;
|
||||
|
||||
disablePromptHint: boolean;
|
||||
|
||||
@@ -140,7 +141,7 @@ const DEFAULT_CONFIG: ChatConfig = {
|
||||
fontSize: 15,
|
||||
theme: Theme.Dark as Theme,
|
||||
tightBorder: false,
|
||||
sendPreviewBubble: false,
|
||||
sendPreviewBubble: true,
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
@@ -205,7 +206,7 @@ interface ChatStore {
|
||||
moveSession: (from: number, to: number) => void;
|
||||
selectSession: (index: number) => void;
|
||||
newSession: () => void;
|
||||
deleteSession: () => void;
|
||||
deleteSession: (index?: number) => void;
|
||||
currentSession: () => ChatSession;
|
||||
onNewMessage: (message: Message) => void;
|
||||
onUserInput: (content: string) => Promise<void>;
|
||||
@@ -326,24 +327,31 @@ export const useChatStore = create<ChatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
deleteSession() {
|
||||
deleteSession(i?: number) {
|
||||
const deletedSession = get().currentSession();
|
||||
const index = get().currentSessionIndex;
|
||||
const index = i ?? get().currentSessionIndex;
|
||||
const isLastSession = get().sessions.length === 1;
|
||||
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
|
||||
get().removeSession(index);
|
||||
|
||||
showToast(
|
||||
Locale.Home.DeleteToast,
|
||||
{
|
||||
text: Locale.Home.Revert,
|
||||
onClick() {
|
||||
set((state) => ({
|
||||
sessions: state.sessions
|
||||
.slice(0, index)
|
||||
.concat([deletedSession])
|
||||
.concat(
|
||||
state.sessions.slice(index + Number(isLastSession)),
|
||||
),
|
||||
}));
|
||||
},
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
showToast(Locale.Home.DeleteToast, {
|
||||
text: Locale.Home.Revert,
|
||||
onClick() {
|
||||
set((state) => ({
|
||||
sessions: state.sessions
|
||||
.slice(0, index)
|
||||
.concat([deletedSession])
|
||||
.concat(state.sessions.slice(index + Number(isLastSession))),
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
currentSession() {
|
||||
|
||||
54
app/utils.ts
54
app/utils.ts
@@ -51,6 +51,12 @@ export function isMobileScreen() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
export function isFirefox() {
|
||||
return (
|
||||
typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent)
|
||||
);
|
||||
}
|
||||
|
||||
export function selectOrCopy(el: HTMLElement, content: string) {
|
||||
const currentSelection = window.getSelection();
|
||||
|
||||
@@ -91,3 +97,51 @@ export function getCurrentVersion() {
|
||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
||||
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
|
||||
}
|
||||
|
||||
function getDomContentWidth(dom: HTMLElement) {
|
||||
const style = window.getComputedStyle(dom);
|
||||
const paddingWidth =
|
||||
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
||||
const width = dom.clientWidth - paddingWidth;
|
||||
return width;
|
||||
}
|
||||
|
||||
function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) {
|
||||
let dom = document.getElementById(id);
|
||||
|
||||
if (!dom) {
|
||||
dom = document.createElement("span");
|
||||
dom.style.position = "absolute";
|
||||
dom.style.wordBreak = "break-word";
|
||||
dom.style.fontSize = "14px";
|
||||
dom.style.transform = "translateY(-200vh)";
|
||||
dom.style.pointerEvents = "none";
|
||||
dom.style.opacity = "0";
|
||||
dom.id = id;
|
||||
document.body.appendChild(dom);
|
||||
init?.(dom);
|
||||
}
|
||||
|
||||
return dom!;
|
||||
}
|
||||
|
||||
export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
||||
const measureDom = getOrCreateMeasureDom("__measure");
|
||||
const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => {
|
||||
dom.innerText = "TEXT_FOR_MEASURE";
|
||||
});
|
||||
|
||||
const width = getDomContentWidth(dom);
|
||||
measureDom.style.width = width + "px";
|
||||
measureDom.innerHTML = dom.value.trim().length > 0 ? dom.value : "1";
|
||||
|
||||
const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1);
|
||||
const height = parseFloat(window.getComputedStyle(measureDom).height);
|
||||
const singleLineHeight = parseFloat(
|
||||
window.getComputedStyle(singleLineDom).height,
|
||||
);
|
||||
|
||||
const rows = Math.round(height / singleLineHeight) + lineWrapCount;
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user