Merge branch 'Yidadaa:main' into main
This commit is contained in:
commit
3ed21b96df
|
@ -1,6 +1,7 @@
|
|||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
@ -9,25 +10,43 @@ jobs:
|
|||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
-
|
||||
name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
-
|
||||
name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: yidadaa/chatgpt-next-web
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
-
|
||||
name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
|
|
@ -6,13 +6,9 @@ RUN apk add --no-cache libc6-compat
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock* package-lock.json* ./
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
RUN yarn install
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
|
|
|
@ -26,13 +26,13 @@
|
|||
@media only screen and (min-width: 600px) {
|
||||
.tight-container {
|
||||
--window-width: 100vw;
|
||||
--window-height: 100vh;
|
||||
--window-height: var(--full-height);
|
||||
--window-content-width: calc(100% - var(--sidebar-width));
|
||||
|
||||
@include container();
|
||||
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
max-height: var(--full-height);
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
|||
position: absolute;
|
||||
left: -100%;
|
||||
z-index: 999;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
transition: all ease 0.3s;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,13 @@ import DownloadIcon from "../icons/download.svg";
|
|||
|
||||
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
|
||||
import { showModal, showToast } from "./ui-lib";
|
||||
import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
isIOS,
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
} from "../utils";
|
||||
import Locale from "../locales";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
@ -239,7 +245,7 @@ export function Chat(props: {
|
|||
if (!dom) return;
|
||||
const paddingBottomNum: number = parseInt(
|
||||
window.getComputedStyle(dom).paddingBottom,
|
||||
10
|
||||
10,
|
||||
);
|
||||
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
||||
};
|
||||
|
@ -313,9 +319,7 @@ export function Chat(props: {
|
|||
|
||||
// for auto-scroll
|
||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// wont scroll while hovering messages
|
||||
const [autoScroll, setAutoScroll] = useState(false);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
|
||||
// preview messages
|
||||
const messages = (session.messages as RenderMessage[])
|
||||
|
@ -348,7 +352,17 @@ export function Chat(props: {
|
|||
useLayoutEffect(() => {
|
||||
setTimeout(() => {
|
||||
const dom = latestMessageRef.current;
|
||||
if (dom && !isIOS() && autoScroll) {
|
||||
const inputDom = inputRef.current;
|
||||
|
||||
// only scroll when input overlaped message body
|
||||
let shouldScroll = true;
|
||||
if (dom && inputDom) {
|
||||
const domRect = dom.getBoundingClientRect();
|
||||
const inputRect = inputDom.getBoundingClientRect();
|
||||
shouldScroll = domRect.top > inputRect.top;
|
||||
}
|
||||
|
||||
if (dom && autoScroll && shouldScroll) {
|
||||
dom.scrollIntoView({
|
||||
block: "end",
|
||||
});
|
||||
|
@ -369,7 +383,7 @@ export function Chat(props: {
|
|||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||
if (newTopic && newTopic !== session.topic) {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.topic = newTopic!)
|
||||
(session) => (session.topic = newTopic!),
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
@ -504,7 +518,7 @@ export function Chat(props: {
|
|||
onFocus={() => setAutoScroll(true)}
|
||||
onBlur={() => {
|
||||
setAutoScroll(false);
|
||||
setTimeout(() => setPromptHints([]), 100);
|
||||
setTimeout(() => setPromptHints([]), 500);
|
||||
}}
|
||||
autoFocus={!props?.sideBarShowing}
|
||||
/>
|
||||
|
@ -633,7 +647,9 @@ export function Home() {
|
|||
return (
|
||||
<div
|
||||
className={`${
|
||||
config.tightBorder ? styles["tight-container"] : styles.container
|
||||
config.tightBorder && !isMobileScreen()
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
import { Avatar, PromptHints } from "./home";
|
||||
|
||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||
import { getCurrentCommitId } from "../utils";
|
||||
import { getCurrentVersion } from "../utils";
|
||||
import Link from "next/link";
|
||||
import { UPDATE_URL } from "../constant";
|
||||
import { SearchService, usePromptStore } from "../store/prompt";
|
||||
|
@ -60,7 +60,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||
|
||||
const updateStore = useUpdateStore();
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentId = getCurrentCommitId();
|
||||
const currentId = getCurrentVersion();
|
||||
const remoteId = updateStore.remoteId;
|
||||
const hasNewVersion = currentId !== remoteId;
|
||||
|
||||
|
@ -267,19 +267,17 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||
></input>
|
||||
</SettingItem>
|
||||
|
||||
<div className="no-mobile">
|
||||
<SettingItem title={Locale.Settings.TightBorder}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.tightBorder}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</SettingItem>
|
||||
</div>
|
||||
<SettingItem title={Locale.Settings.TightBorder}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.tightBorder}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.tightBorder = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</SettingItem>
|
||||
</List>
|
||||
<List>
|
||||
<SettingItem
|
||||
|
@ -375,7 +373,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||
type="range"
|
||||
title={config.historyMessageCount.toString()}
|
||||
value={config.historyMessageCount}
|
||||
min="2"
|
||||
min="0"
|
||||
max="25"
|
||||
step="2"
|
||||
onChange={(e) =>
|
||||
|
|
|
@ -3,3 +3,4 @@ export const REPO = "ChatGPT-Next-Web";
|
|||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||
export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
|
||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||
|
|
|
@ -8,11 +8,12 @@ import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
|||
let COMMIT_ID: string | undefined;
|
||||
try {
|
||||
COMMIT_ID = process
|
||||
// .execSync("git describe --tags --abbrev=0")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("No git or not from git repo.")
|
||||
console.error("No git or not from git repo.");
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
|
@ -22,13 +23,13 @@ export const metadata = {
|
|||
title: "ChatGPT Next Web",
|
||||
statusBarStyle: "black-translucent",
|
||||
},
|
||||
themeColor: "#fafafa"
|
||||
themeColor: "#fafafa",
|
||||
};
|
||||
|
||||
function Meta() {
|
||||
const metas = {
|
||||
version: COMMIT_ID ?? "unknown",
|
||||
access: (ACCESS_CODES.size > 0 || IS_IN_DOCKER) ? "enabled" : "disabled",
|
||||
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -89,7 +89,9 @@ export function isValidNumber(x: number, min: number, max: number) {
|
|||
return typeof x === "number" && x <= max && x >= min;
|
||||
}
|
||||
|
||||
export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
|
||||
export function filterConfig(oldConfig: ModelConfig): Partial<ModelConfig> {
|
||||
const config = Object.assign({}, oldConfig);
|
||||
|
||||
const validator: {
|
||||
[k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean;
|
||||
} = {
|
||||
|
@ -103,7 +105,7 @@ export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
|
|||
return isValidNumber(x as number, -2, 2);
|
||||
},
|
||||
temperature(x) {
|
||||
return isValidNumber(x as number, 0, 1);
|
||||
return isValidNumber(x as number, 0, 2);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { FETCH_COMMIT_URL } from "../constant";
|
||||
import { getCurrentCommitId } from "../utils";
|
||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
|
||||
import { getCurrentVersion } from "../utils";
|
||||
|
||||
export interface UpdateStore {
|
||||
lastUpdate: number;
|
||||
|
@ -19,16 +19,17 @@ export const useUpdateStore = create<UpdateStore>()(
|
|||
remoteId: "",
|
||||
|
||||
async getLatestCommitId(force = false) {
|
||||
const overOneHour = Date.now() - get().lastUpdate > 3600 * 1000;
|
||||
const shouldFetch = force || overOneHour;
|
||||
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
|
||||
const shouldFetch = force || overTenMins;
|
||||
if (!shouldFetch) {
|
||||
return getCurrentCommitId();
|
||||
return getCurrentVersion();
|
||||
}
|
||||
|
||||
try {
|
||||
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
||||
// const remoteId = data[0].name as string;
|
||||
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
||||
const sha = data[0].sha as string;
|
||||
const remoteId = sha.substring(0, 7);
|
||||
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||
set(() => ({
|
||||
lastUpdate: Date.now(),
|
||||
remoteId,
|
||||
|
@ -37,13 +38,13 @@ export const useUpdateStore = create<UpdateStore>()(
|
|||
return remoteId;
|
||||
} catch (error) {
|
||||
console.error("[Fetch Upstream Commit Id]", error);
|
||||
return getCurrentCommitId();
|
||||
return getCurrentVersion();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: UPDATE_KEY,
|
||||
version: 1,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -53,12 +53,13 @@
|
|||
--sidebar-width: 300px;
|
||||
--window-content-width: calc(100% - var(--sidebar-width));
|
||||
--message-max-width: 80%;
|
||||
--full-height: 100vh;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
:root {
|
||||
--window-width: 100vw;
|
||||
--window-height: 100vh;
|
||||
--window-height: var(--full-height);
|
||||
--sidebar-width: 100vw;
|
||||
--window-content-width: var(--window-width);
|
||||
--message-max-width: 100%;
|
||||
|
@ -80,14 +81,14 @@ body {
|
|||
color: var(--black);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
background-color: var(--second);
|
||||
|
@ -119,6 +120,11 @@ select {
|
|||
cursor: pointer;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
|
@ -196,7 +202,7 @@ div.math {
|
|||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
height: var(--full-height);
|
||||
width: 100vw;
|
||||
background-color: rgba($color: #000000, $alpha: 0.5);
|
||||
display: flex;
|
||||
|
|
|
@ -120,33 +120,3 @@
|
|||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
.markdown-body pre {
|
||||
filter: invert(1) hue-rotate(90deg) brightness(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
.markdown-body pre {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
@include light();
|
||||
}
|
||||
|
||||
.light {
|
||||
@include light();
|
||||
}
|
||||
|
||||
.dark {
|
||||
@include dark();
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
@include dark();
|
||||
}
|
||||
}
|
||||
|
|
12
app/utils.ts
12
app/utils.ts
|
@ -4,7 +4,7 @@ import Locale from "./locales";
|
|||
export function trimTopic(topic: string) {
|
||||
const s = topic.split("");
|
||||
let lastChar = s.at(-1); // 获取 s 的最后一个字符
|
||||
let pattern = /[,。!?、]/; // 定义匹配中文标点符号的正则表达式
|
||||
let pattern = /[,。!?、,.!?]/; // 定义匹配中文和英文标点符号的正则表达式
|
||||
while (lastChar && pattern.test(lastChar!)) {
|
||||
s.pop();
|
||||
lastChar = s.at(-1);
|
||||
|
@ -28,7 +28,7 @@ export function downloadAs(text: string, filename: string) {
|
|||
const element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
|
@ -45,6 +45,10 @@ export function isIOS() {
|
|||
return /iphone|ipad|ipod/.test(userAgent);
|
||||
}
|
||||
|
||||
export function isMobileScreen() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
export function selectOrCopy(el: HTMLElement, content: string) {
|
||||
const currentSelection = window.getSelection();
|
||||
|
||||
|
@ -61,7 +65,7 @@ export function queryMeta(key: string, defaultValue?: string): string {
|
|||
let ret: string;
|
||||
if (document) {
|
||||
const meta = document.head.querySelector(
|
||||
`meta[name='${key}']`
|
||||
`meta[name='${key}']`,
|
||||
) as HTMLMetaElement;
|
||||
ret = meta?.content ?? "";
|
||||
} else {
|
||||
|
@ -72,7 +76,7 @@ export function queryMeta(key: string, defaultValue?: string): string {
|
|||
}
|
||||
|
||||
let currentId: string;
|
||||
export function getCurrentCommitId() {
|
||||
export function getCurrentVersion() {
|
||||
if (currentId) {
|
||||
return currentId;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import fetch from "node-fetch";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const CN_URL =
|
||||
const RAW_CN_URL =
|
||||
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
|
||||
const EN_URL =
|
||||
const CN_URL =
|
||||
"https://cdn.jsdelivr.net/gh/PlexPt/awesome-chatgpt-prompts-zh@main/prompts-zh.json";
|
||||
const RAW_EN_URL =
|
||||
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
|
||||
const EN_URL =
|
||||
"https://cdn.jsdelivr.net/gh/f/awesome-chatgpt-prompts@main/prompts.csv";
|
||||
const FILE = "./public/prompts.json";
|
||||
|
||||
async function fetchCN() {
|
||||
|
|
Loading…
Reference in New Issue