This commit is contained in:
[witbox2018]
2023-04-10 09:55:33 +08:00
parent c306bd4823
commit a624f83821
21 changed files with 386 additions and 49 deletions

View File

@@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) {
},
{
status: 500,
}
},
);
}
}

View File

@@ -50,4 +50,7 @@
.icon-button-text {
margin-left: 5px;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -96,7 +96,7 @@ export function ChatList() {
index={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={chatStore.deleteSession}
onDelete={() => chatStore.deleteSession(i)}
/>
))}
{provided.placeholder}

View File

@@ -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 {

View File

@@ -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"]}>

View File

@@ -67,7 +67,7 @@ export function Markdown(props: { content: string }) {
components={{
pre: PreCode,
}}
linkTarget={'_blank'}
linkTarget={"_blank"}
>
{props.content}
</ReactMarkdown>

View File

@@ -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%;
}
}

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;
}