@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Deployment**
|
||||||
|
- [ ] Docker
|
||||||
|
- [ ] Vercel
|
||||||
|
- [ ] Server
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional Logs**
|
||||||
|
Add any logs about the problem here.
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: 功能建议
|
||||||
|
about: 请告诉我们你的灵光一闪
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**这个功能与现有的问题有关吗?**
|
||||||
|
如果有关,请在此列出链接或者描述问题。
|
||||||
|
|
||||||
|
**你想要什么功能或者有什么建议?**
|
||||||
|
尽管告诉我们。
|
||||||
|
|
||||||
|
**有没有可以参考的同类竞品?**
|
||||||
|
可以给出参考产品的链接或者截图。
|
||||||
|
|
||||||
|
**其他信息**
|
||||||
|
可以说说你的其他考虑。
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: 反馈问题
|
||||||
|
about: 请告诉我们你遇到的问题
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**描述问题**
|
||||||
|
请在此描述你遇到了什么问题。
|
||||||
|
|
||||||
|
**如何复现**
|
||||||
|
请告诉我们你是通过什么操作触发的该问题。
|
||||||
|
|
||||||
|
**截图**
|
||||||
|
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
|
||||||
|
|
||||||
|
**一些必要的信息**
|
||||||
|
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
|
||||||
|
- 浏览器: [比如 chrome, safari]
|
||||||
|
- 版本: [填写设置页面的版本号]
|
||||||
|
- 部署方式:[比如 vercel、docker 或者服务器部署]
|
||||||
@ -1,29 +1,33 @@
|
|||||||
name: Upstream Sync
|
name: Upstream Sync
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */12 * * *' # every 12 hours
|
- cron: "0 */6 * * *" # every 6 hours
|
||||||
workflow_dispatch: # on button click
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync_latest_from_upstream:
|
sync_latest_from_upstream:
|
||||||
name: Sync latest commits from upstream repo
|
name: Sync latest commits from upstream repo
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.repository.fork }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Step 1: run a standard checkout action, provided by github
|
# Step 1: run a standard checkout action
|
||||||
- name: Checkout target repo
|
- name: Checkout target repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Step 2: run the sync action
|
||||||
|
- name: Sync upstream changes
|
||||||
|
id: sync
|
||||||
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||||
|
with:
|
||||||
|
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
||||||
|
upstream_sync_branch: main
|
||||||
|
target_sync_branch: main
|
||||||
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||||
|
|
||||||
# Step 2: run the sync action
|
# Set test_mode true to run tests instead of the true action!!
|
||||||
- name: Sync upstream changes
|
test_mode: false
|
||||||
id: sync
|
|
||||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
|
||||||
with:
|
|
||||||
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
|
||||||
upstream_sync_branch: main
|
|
||||||
target_sync_branch: main
|
|
||||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
|
||||||
|
|
||||||
# Set test_mode true to run tests instead of the true action!!
|
|
||||||
test_mode: false
|
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
||||||
}
|
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
flynn.zhang@foxmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
import styles from "./home.module.scss";
|
||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Droppable,
|
||||||
|
Draggable,
|
||||||
|
OnDragEndResponder,
|
||||||
|
} from "@hello-pangea/dnd";
|
||||||
|
|
||||||
|
import { useChatStore } from "../store";
|
||||||
|
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { isMobileScreen } from "../utils";
|
||||||
|
|
||||||
|
export function ChatItem(props: {
|
||||||
|
onClick?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
title: string;
|
||||||
|
count: number;
|
||||||
|
time: string;
|
||||||
|
selected: boolean;
|
||||||
|
id: number;
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
className={`${styles["chat-item"]} ${
|
||||||
|
props.selected && styles["chat-item-selected"]
|
||||||
|
}`}
|
||||||
|
onClick={props.onClick}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||||
|
<div className={styles["chat-item-info"]}>
|
||||||
|
<div className={styles["chat-item-count"]}>
|
||||||
|
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||||
|
</div>
|
||||||
|
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatList() {
|
||||||
|
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
|
||||||
|
useChatStore((state) => [
|
||||||
|
state.sessions,
|
||||||
|
state.currentSessionIndex,
|
||||||
|
state.selectSession,
|
||||||
|
state.removeSession,
|
||||||
|
state.moveSession,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onDragEnd: OnDragEndResponder = (result) => {
|
||||||
|
const { destination, source } = result;
|
||||||
|
if (!destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
destination.droppableId === source.droppableId &&
|
||||||
|
destination.index === source.index
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSession(source.index, destination.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="chat-list">
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
className={styles["chat-list"]}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
>
|
||||||
|
{sessions.map((item, i) => (
|
||||||
|
<ChatItem
|
||||||
|
title={item.topic}
|
||||||
|
time={item.lastUpdate}
|
||||||
|
count={item.messages.length}
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
index={i}
|
||||||
|
selected={i === selectedIndex}
|
||||||
|
onClick={() => selectSession(i)}
|
||||||
|
onDelete={() =>
|
||||||
|
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
|
||||||
|
removeSession(i)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.prompt-toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -50px;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
|
||||||
|
.prompt-toast-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
border: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
animation: slide-in-from-top ease 0.3s;
|
||||||
|
|
||||||
|
.prompt-toast-content {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-prompt {
|
||||||
|
.context-prompt-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.context-role {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-content {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-delete-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-prompt-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-prompt {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.memory-prompt-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.memory-prompt-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-prompt-content {
|
||||||
|
background-color: var(--gray);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,689 @@
|
|||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||||
|
|
||||||
|
import SendWhiteIcon from "../icons/send-white.svg";
|
||||||
|
import BrainIcon from "../icons/brain.svg";
|
||||||
|
import ExportIcon from "../icons/export.svg";
|
||||||
|
import MenuIcon from "../icons/menu.svg";
|
||||||
|
import CopyIcon from "../icons/copy.svg";
|
||||||
|
import DownloadIcon from "../icons/download.svg";
|
||||||
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
import BotIcon from "../icons/bot.svg";
|
||||||
|
import AddIcon from "../icons/add.svg";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Message,
|
||||||
|
SubmitKey,
|
||||||
|
useChatStore,
|
||||||
|
BOT_HELLO,
|
||||||
|
ROLES,
|
||||||
|
createMessage,
|
||||||
|
} from "../store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
downloadAs,
|
||||||
|
getEmojiUrl,
|
||||||
|
isMobileScreen,
|
||||||
|
selectOrCopy,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
import { ControllerPool } from "../requests";
|
||||||
|
import { Prompt, usePromptStore } from "../store/prompt";
|
||||||
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import styles from "./home.module.scss";
|
||||||
|
import chatStyle from "./chat.module.scss";
|
||||||
|
|
||||||
|
import { Input, Modal, showModal, showToast } from "./ui-lib";
|
||||||
|
|
||||||
|
const Markdown = dynamic(
|
||||||
|
async () => memo((await import("./markdown")).Markdown),
|
||||||
|
{
|
||||||
|
loading: () => <LoadingIcon />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
||||||
|
loading: () => <LoadingIcon />,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function Avatar(props: { role: Message["role"] }) {
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
|
if (props.role !== "user") {
|
||||||
|
return <BotIcon className={styles["user-avtar"]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["user-avtar"]}>
|
||||||
|
<Emoji unified={config.avatar} size={18} getEmojiUrl={getEmojiUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportMessages(messages: Message[], topic: string) {
|
||||||
|
const mdText =
|
||||||
|
`# ${topic}\n\n` +
|
||||||
|
messages
|
||||||
|
.map((m) => {
|
||||||
|
return m.role === "user"
|
||||||
|
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
||||||
|
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
const filename = `${topic}.md`;
|
||||||
|
|
||||||
|
showModal({
|
||||||
|
title: Locale.Export.Title,
|
||||||
|
children: (
|
||||||
|
<div className="markdown-body">
|
||||||
|
<pre className={styles["export-content"]}>{mdText}</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Export.Copy}
|
||||||
|
onClick={() => copyToClipboard(mdText)}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="download"
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Export.Download}
|
||||||
|
onClick={() => downloadAs(mdText, filename)}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromptToast(props: {
|
||||||
|
showToast?: boolean;
|
||||||
|
showModal?: boolean;
|
||||||
|
setShowModal: (_: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const context = session.context;
|
||||||
|
|
||||||
|
const addContextPrompt = (prompt: Message) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context.push(prompt);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeContextPrompt = (i: number) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context.splice(i, 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContextPrompt = (i: number, prompt: Message) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context[i] = prompt;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
||||||
|
{props.showToast && (
|
||||||
|
<div
|
||||||
|
className={chatStyle["prompt-toast-inner"] + " clickable"}
|
||||||
|
role="button"
|
||||||
|
onClick={() => props.setShowModal(true)}
|
||||||
|
>
|
||||||
|
<BrainIcon />
|
||||||
|
<span className={chatStyle["prompt-toast-content"]}>
|
||||||
|
{Locale.Context.Toast(context.length)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.showModal && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Context.Edit}
|
||||||
|
onClose={() => props.setShowModal(false)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="reset"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Memory.Reset}
|
||||||
|
onClick={() =>
|
||||||
|
confirm(Locale.Memory.ResetConfirm) &&
|
||||||
|
chatStore.resetSession()
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Memory.Copy}
|
||||||
|
onClick={() => copyToClipboard(session.memoryPrompt)}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div className={chatStyle["context-prompt"]}>
|
||||||
|
{context.map((c, i) => (
|
||||||
|
<div className={chatStyle["context-prompt-row"]} key={i}>
|
||||||
|
<select
|
||||||
|
value={c.role}
|
||||||
|
className={chatStyle["context-role"]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateContextPrompt(i, {
|
||||||
|
...c,
|
||||||
|
role: e.target.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ROLES.map((r) => (
|
||||||
|
<option key={r} value={r}>
|
||||||
|
{r}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Input
|
||||||
|
value={c.content}
|
||||||
|
type="text"
|
||||||
|
className={chatStyle["context-content"]}
|
||||||
|
rows={1}
|
||||||
|
onInput={(e) =>
|
||||||
|
updateContextPrompt(i, {
|
||||||
|
...c,
|
||||||
|
content: e.currentTarget.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={chatStyle["context-delete-button"]}
|
||||||
|
onClick={() => removeContextPrompt(i)}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={chatStyle["context-prompt-row"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text={Locale.Context.Add}
|
||||||
|
bordered
|
||||||
|
className={chatStyle["context-prompt-button"]}
|
||||||
|
onClick={() =>
|
||||||
|
addContextPrompt({
|
||||||
|
role: "system",
|
||||||
|
content: "",
|
||||||
|
date: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={chatStyle["memory-prompt"]}>
|
||||||
|
<div className={chatStyle["memory-prompt-title"]}>
|
||||||
|
<span>
|
||||||
|
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
|
||||||
|
{session.messages.length})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<label className={chatStyle["memory-prompt-action"]}>
|
||||||
|
{Locale.Memory.Send}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={session.sendMemory}
|
||||||
|
onChange={() =>
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) =>
|
||||||
|
(session.sendMemory = !session.sendMemory),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={chatStyle["memory-prompt-content"]}>
|
||||||
|
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSubmitHandler() {
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
const submitKey = config.submitKey;
|
||||||
|
|
||||||
|
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key !== "Enter") return false;
|
||||||
|
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
||||||
|
return (
|
||||||
|
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||||
|
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||||
|
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
|
||||||
|
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
|
||||||
|
(config.submitKey === SubmitKey.Enter &&
|
||||||
|
!e.altKey &&
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!e.metaKey)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitKey,
|
||||||
|
shouldSubmit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptHints(props: {
|
||||||
|
prompts: Prompt[];
|
||||||
|
onPromptSelect: (prompt: Prompt) => void;
|
||||||
|
}) {
|
||||||
|
if (props.prompts.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["prompt-hints"]}>
|
||||||
|
{props.prompts.map((prompt, i) => (
|
||||||
|
<div
|
||||||
|
className={styles["prompt-hint"]}
|
||||||
|
key={prompt.title + i.toString()}
|
||||||
|
onClick={() => props.onPromptSelect(prompt)}
|
||||||
|
>
|
||||||
|
<div className={styles["hint-title"]}>{prompt.title}</div>
|
||||||
|
<div className={styles["hint-content"]}>{prompt.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useScrollToBottom() {
|
||||||
|
// for auto-scroll
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
|
||||||
|
// auto scroll
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const dom = scrollRef.current;
|
||||||
|
if (dom && autoScroll) {
|
||||||
|
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollRef,
|
||||||
|
autoScroll,
|
||||||
|
setAutoScroll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chat(props: {
|
||||||
|
showSideBar?: () => void;
|
||||||
|
sideBarShowing?: boolean;
|
||||||
|
}) {
|
||||||
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const [session, sessionIndex] = useChatStore((state) => [
|
||||||
|
state.currentSession(),
|
||||||
|
state.currentSessionIndex,
|
||||||
|
]);
|
||||||
|
const fontSize = useChatStore((state) => state.config.fontSize);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [userInput, setUserInput] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
|
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
||||||
|
const [hitBottom, setHitBottom] = useState(false);
|
||||||
|
|
||||||
|
const onChatBodyScroll = (e: HTMLElement) => {
|
||||||
|
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||||
|
setHitBottom(isTouchBottom);
|
||||||
|
};
|
||||||
|
|
||||||
|
// prompt hints
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||||
|
const onSearch = useDebouncedCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setPromptHints(promptStore.search(text));
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
{ leading: true, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPromptSelect = (prompt: Prompt) => {
|
||||||
|
setUserInput(prompt.content);
|
||||||
|
setPromptHints([]);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollInput = () => {
|
||||||
|
const dom = inputRef.current;
|
||||||
|
if (!dom) return;
|
||||||
|
const paddingBottomNum: number = parseInt(
|
||||||
|
window.getComputedStyle(dom).paddingBottom,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
||||||
|
};
|
||||||
|
|
||||||
|
// only search prompts when user input is short
|
||||||
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
|
const onInput = (text: string) => {
|
||||||
|
scrollInput();
|
||||||
|
setUserInput(text);
|
||||||
|
const n = text.trim().length;
|
||||||
|
|
||||||
|
// clear search results
|
||||||
|
if (n === 0) {
|
||||||
|
setPromptHints([]);
|
||||||
|
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||||
|
// check if need to trigger auto completion
|
||||||
|
if (text.startsWith("/")) {
|
||||||
|
let searchText = text.slice(1);
|
||||||
|
if (searchText.length === 0) {
|
||||||
|
searchText = " ";
|
||||||
|
}
|
||||||
|
onSearch(searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// submit user input
|
||||||
|
const onUserSubmit = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
|
setUserInput("");
|
||||||
|
setPromptHints([]);
|
||||||
|
if (!isMobileScreen()) inputRef.current?.focus();
|
||||||
|
setAutoScroll(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// stop response
|
||||||
|
const onUserStop = (messageId: number) => {
|
||||||
|
ControllerPool.stop(sessionIndex, messageId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if should send message
|
||||||
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (shouldSubmit(e)) {
|
||||||
|
setAutoScroll(true);
|
||||||
|
onUserSubmit();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onRightClick = (e: any, message: Message) => {
|
||||||
|
// auto fill user input
|
||||||
|
if (message.role === "user") {
|
||||||
|
setUserInput(message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy to clipboard
|
||||||
|
if (selectOrCopy(e.currentTarget, message.content)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = (botIndex: number) => {
|
||||||
|
// find last user input message and resend
|
||||||
|
for (let i = botIndex; i >= 0; i -= 1) {
|
||||||
|
if (messages[i].role === "user") {
|
||||||
|
setIsLoading(true);
|
||||||
|
chatStore
|
||||||
|
.onUserInput(messages[i].content)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
|
chatStore.updateCurrentSession((session) =>
|
||||||
|
session.messages.splice(i, 2),
|
||||||
|
);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
|
const context: RenderMessage[] = session.context.slice();
|
||||||
|
|
||||||
|
if (
|
||||||
|
context.length === 0 &&
|
||||||
|
session.messages.at(0)?.content !== BOT_HELLO.content
|
||||||
|
) {
|
||||||
|
context.push(BOT_HELLO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preview messages
|
||||||
|
const messages = context
|
||||||
|
.concat(session.messages as RenderMessage[])
|
||||||
|
.concat(
|
||||||
|
isLoading
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
...createMessage({
|
||||||
|
role: "assistant",
|
||||||
|
content: "……",
|
||||||
|
}),
|
||||||
|
preview: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
|
userInput.length > 0 && config.sendPreviewBubble
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
...createMessage({
|
||||||
|
role: "user",
|
||||||
|
content: userInput,
|
||||||
|
}),
|
||||||
|
preview: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
|
// Auto focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.sideBarShowing && isMobileScreen()) return;
|
||||||
|
inputRef.current?.focus();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chat} key={session.id}>
|
||||||
|
<div className={styles["window-header"]}>
|
||||||
|
<div
|
||||||
|
className={styles["window-header-title"]}
|
||||||
|
onClick={props?.showSideBar}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
|
onClick={() => {
|
||||||
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
|
if (newTopic && newTopic !== session.topic) {
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.topic = newTopic!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.topic}
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-header-sub-title"]}>
|
||||||
|
{Locale.Chat.SubTitle(session.messages.length)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-actions"]}>
|
||||||
|
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
||||||
|
<IconButton
|
||||||
|
icon={<MenuIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.ChatList}
|
||||||
|
onClick={props?.showSideBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<BrainIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.CompressedHistory}
|
||||||
|
onClick={() => {
|
||||||
|
setShowPromptModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ExportIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.Export}
|
||||||
|
onClick={() => {
|
||||||
|
exportMessages(
|
||||||
|
session.messages.filter((msg) => !msg.isError),
|
||||||
|
session.topic,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PromptToast
|
||||||
|
showToast={!hitBottom}
|
||||||
|
showModal={showPromptModal}
|
||||||
|
setShowModal={setShowPromptModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["chat-body"]}
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||||
|
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
|
||||||
|
onTouchStart={() => {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
setAutoScroll(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.map((message, i) => {
|
||||||
|
const isUser = message.role === "user";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={
|
||||||
|
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles["chat-message-container"]}>
|
||||||
|
<div className={styles["chat-message-avatar"]}>
|
||||||
|
<Avatar role={message.role} />
|
||||||
|
</div>
|
||||||
|
{(message.preview || message.streaming) && (
|
||||||
|
<div className={styles["chat-message-status"]}>
|
||||||
|
{Locale.Chat.Typing}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles["chat-message-item"]}>
|
||||||
|
{!isUser &&
|
||||||
|
!(message.preview || message.content.length === 0) && (
|
||||||
|
<div className={styles["chat-message-top-actions"]}>
|
||||||
|
{message.streaming ? (
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onUserStop(message.id ?? i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Stop}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onResend(i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Retry}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => copyToClipboard(message.content)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Copy}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(message.preview || message.content.length === 0) &&
|
||||||
|
!isUser ? (
|
||||||
|
<LoadingIcon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="markdown-body"
|
||||||
|
style={{ fontSize: `${fontSize}px` }}
|
||||||
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
|
onDoubleClickCapture={() => {
|
||||||
|
if (!isMobileScreen()) return;
|
||||||
|
setUserInput(message.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Markdown content={message.content} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isUser && !message.preview && (
|
||||||
|
<div className={styles["chat-message-actions"]}>
|
||||||
|
<div className={styles["chat-message-action-date"]}>
|
||||||
|
{message.date.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["chat-input-panel"]}>
|
||||||
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
className={styles["chat-input"]}
|
||||||
|
placeholder={Locale.Chat.Input(submitKey)}
|
||||||
|
rows={2}
|
||||||
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
|
value={userInput}
|
||||||
|
onKeyDown={onInputKeyDown}
|
||||||
|
onFocus={() => setAutoScroll(isMobileScreen())}
|
||||||
|
onBlur={() => {
|
||||||
|
setAutoScroll(false);
|
||||||
|
setTimeout(() => setPromptHints([]), 500);
|
||||||
|
}}
|
||||||
|
autoFocus={!props?.sideBarShowing}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<SendWhiteIcon />}
|
||||||
|
text={Locale.Chat.Send}
|
||||||
|
className={styles["chat-input-send"]}
|
||||||
|
noDark
|
||||||
|
disabled={!userInput}
|
||||||
|
onClick={onUserSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
import { ISSUE_URL } from "../constant";
|
||||||
|
|
||||||
|
interface IErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
info: React.ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null, info: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
// Update state with error details
|
||||||
|
this.setState({ hasError: true, error, info });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Render error message
|
||||||
|
return (
|
||||||
|
<div className="error">
|
||||||
|
<h2>Oops, something went wrong!</h2>
|
||||||
|
<pre>
|
||||||
|
<code>{this.state.error?.toString()}</code>
|
||||||
|
<code>{this.state.info?.componentStack}</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<a href={ISSUE_URL} className="report">
|
||||||
|
<IconButton
|
||||||
|
text="Report This Error"
|
||||||
|
icon={<GithubIcon />}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if no error occurred, render children
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
.input-range {
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px 15px 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import styles from "./input-range.module.scss";
|
||||||
|
|
||||||
|
interface InputRangeProps {
|
||||||
|
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||||
|
title?: string;
|
||||||
|
value: number | string;
|
||||||
|
className?: string;
|
||||||
|
min: string;
|
||||||
|
max: string;
|
||||||
|
step: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputRange({
|
||||||
|
onChange,
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
}: InputRangeProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles["input-range"] + ` ${className ?? ""}`}>
|
||||||
|
{title || value}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
title={title}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onChange={onChange}
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
export const OWNER = "Yidadaa";
|
export const OWNER = "Yidadaa";
|
||||||
export const REPO = "ChatGPT-Next-Web";
|
export const REPO = "ChatGPT-Next-Web";
|
||||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
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 ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
||||||
|
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
|
||||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
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`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,171 @@
|
|||||||
|
import { SubmitKey } from "../store/app";
|
||||||
|
import type { LocaleType } from "./index";
|
||||||
|
|
||||||
|
const it: LocaleType = {
|
||||||
|
WIP: "Work in progress...",
|
||||||
|
Error: {
|
||||||
|
Unauthorized:
|
||||||
|
"Accesso non autorizzato, inserire il codice di accesso nella pagina delle impostazioni.",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} messaggi`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} messaggi con ChatGPT`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: "Vai alla Chat List",
|
||||||
|
CompressedHistory: "Prompt di memoria della cronologia compressa",
|
||||||
|
Export: "Esportazione di tutti i messaggi come Markdown",
|
||||||
|
Copy: "Copia",
|
||||||
|
Stop: "Stop",
|
||||||
|
Retry: "Riprova",
|
||||||
|
},
|
||||||
|
Rename: "Rinomina Chat",
|
||||||
|
Typing: "Typing…",
|
||||||
|
Input: (submitKey: string) => {
|
||||||
|
var inputHints = `Scrivi qualcosa e premi ${submitKey} per inviare`;
|
||||||
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
|
inputHints += ", premi Shift + Enter per andare a capo";
|
||||||
|
}
|
||||||
|
return inputHints;
|
||||||
|
},
|
||||||
|
Send: "Invia",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "Tutti i messaggi",
|
||||||
|
Copy: "Copia tutto",
|
||||||
|
Download: "Scarica",
|
||||||
|
MessageFromYou: "Messaggio da te",
|
||||||
|
MessageFromChatGPT: "Messaggio da ChatGPT",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "Prompt di memoria",
|
||||||
|
EmptyContent: "Vuoto.",
|
||||||
|
Copy: "Copia tutto",
|
||||||
|
Send: "Send Memory",
|
||||||
|
Reset: "Reset Session",
|
||||||
|
ResetConfirm:
|
||||||
|
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "Nuova Chat",
|
||||||
|
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "Impostazioni",
|
||||||
|
SubTitle: "Tutte le impostazioni",
|
||||||
|
Actions: {
|
||||||
|
ClearAll: "Cancella tutti i dati",
|
||||||
|
ResetAll: "Resetta tutte le impostazioni",
|
||||||
|
Close: "Chiudi",
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "Lingue",
|
||||||
|
Options: {
|
||||||
|
cn: "简体中文",
|
||||||
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: "Avatar",
|
||||||
|
FontSize: {
|
||||||
|
Title: "Dimensione carattere",
|
||||||
|
SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
|
||||||
|
},
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `Versione: ${x}`,
|
||||||
|
IsLatest: "Ultima versione",
|
||||||
|
CheckUpdate: "Controlla aggiornamenti",
|
||||||
|
IsChecking: "Sto controllando gli aggiornamenti...",
|
||||||
|
FoundUpdate: (x: string) => `Trovata nuova versione: ${x}`,
|
||||||
|
GoToUpdate: "Aggiorna",
|
||||||
|
},
|
||||||
|
SendKey: "Tasto invia",
|
||||||
|
Theme: "tema",
|
||||||
|
TightBorder: "Bordi stretti",
|
||||||
|
SendPreviewBubble: "Invia l'anteprima della bolla",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Disabilita l'auto completamento",
|
||||||
|
SubTitle: "Input / per attivare il completamento automatico",
|
||||||
|
},
|
||||||
|
List: "Elenco dei suggerimenti",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} built-in, ${custom} user-defined`,
|
||||||
|
Edit: "Modifica",
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "Conteggio dei messaggi allegati",
|
||||||
|
SubTitle: "Numero di messaggi inviati allegati per richiesta",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "Soglia di compressione della cronologia",
|
||||||
|
SubTitle:
|
||||||
|
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
||||||
|
},
|
||||||
|
Token: {
|
||||||
|
Title: "Chiave API",
|
||||||
|
SubTitle:
|
||||||
|
"Utilizzare la chiave per ignorare il limite del codice di accesso",
|
||||||
|
Placeholder: "OpenAI API Key",
|
||||||
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "Bilancio Account",
|
||||||
|
SubTitle(used: any, total: any) {
|
||||||
|
return `Usato in questo mese $${used}, subscription $${total}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Controllando...",
|
||||||
|
Check: "Controlla ancora",
|
||||||
|
NoAccess: "Inserire la chiave API per controllare il saldo",
|
||||||
|
},
|
||||||
|
AccessCode: {
|
||||||
|
Title: "Codice d'accesso",
|
||||||
|
SubTitle: "Controllo d'accesso abilitato",
|
||||||
|
Placeholder: "Inserisci il codice d'accesso",
|
||||||
|
},
|
||||||
|
Model: "Modello GPT",
|
||||||
|
Temperature: {
|
||||||
|
Title: "Temperature",
|
||||||
|
SubTitle: "Un valore maggiore rende l'output più casuale",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "Token massimi",
|
||||||
|
SubTitle: "Lunghezza massima dei token in ingresso e dei token generati",
|
||||||
|
},
|
||||||
|
PresencePenlty: {
|
||||||
|
Title: "Penalità di presenza",
|
||||||
|
SubTitle:
|
||||||
|
"Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "Nuova conversazione",
|
||||||
|
BotHello: "Ciao, come posso aiutarti oggi?",
|
||||||
|
Error: "Qualcosa è andato storto, riprova più tardi.",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"Questo è un riassunto della cronologia delle chat tra l'IA e l'utente:" +
|
||||||
|
content,
|
||||||
|
Topic:
|
||||||
|
"Si prega di generare un titolo di quattro o cinque parole che riassuma la nostra conversazione senza alcuna traccia, punteggiatura, virgolette, punti, simboli o testo aggiuntivo. Rimuovere le virgolette",
|
||||||
|
Summarize:
|
||||||
|
"Riassumi brevemente la nostra discussione in 200 caratteri o meno per usarla come spunto per una futura conversazione.",
|
||||||
|
},
|
||||||
|
ConfirmClearAll:
|
||||||
|
"Confermi la cancellazione di tutti i dati della chat e delle impostazioni?",
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "Copiato sugli appunti",
|
||||||
|
Failed:
|
||||||
|
"Copia fallita, concedere l'autorizzazione all'accesso agli appunti",
|
||||||
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `Con ${x} prompts contestuali`,
|
||||||
|
Edit: "Prompt contestuali e di memoria",
|
||||||
|
Add: "Aggiungi altro",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default it;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
declare global {
|
||||||
|
interface Array<T> {
|
||||||
|
at(index: number): T | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
Array.prototype.at = function (index: number) {
|
||||||
|
// Get the length of the array
|
||||||
|
const length = this.length;
|
||||||
|
|
||||||
|
// Convert negative index to a positive index
|
||||||
|
if (index < 0) {
|
||||||
|
index = length + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return undefined if the index is out of range
|
||||||
|
if (index < 0 || index >= length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Array.prototype.slice method to get value at the specified index
|
||||||
|
return Array.prototype.slice.call(this, index, index + 1)[0];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
@keyframes slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-from-top {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
.markdown-body {
|
||||||
|
pre {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs,
|
||||||
|
pre {
|
||||||
|
background: #1a1b26;
|
||||||
|
color: #cbd2ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Theme: Tokyo-night-Dark
|
||||||
|
origin: https://github.com/enkia/tokyo-night-vscode-theme
|
||||||
|
Description: Original highlight.js style
|
||||||
|
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
|
||||||
|
License: see project LICENSE
|
||||||
|
Touched: 2022
|
||||||
|
*/
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-meta {
|
||||||
|
color: #565f89;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-variable.language_ {
|
||||||
|
color: #f7768e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-params,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #ff9e64;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-built_in {
|
||||||
|
color: #e0af68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-property,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-title.class_,
|
||||||
|
.hljs-title.class_.inherited__,
|
||||||
|
.hljs-title.function_ {
|
||||||
|
color: #7dcfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #73daca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #9ece6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-code,
|
||||||
|
.hljs-formula,
|
||||||
|
.hljs-section {
|
||||||
|
color: #7aa2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-char.escape_,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-operator {
|
||||||
|
color: #bb9af7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-punctuation {
|
||||||
|
color: #c0caf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,122 +0,0 @@
|
|||||||
.markdown-body {
|
|
||||||
pre {
|
|
||||||
background: #282a36;
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
color: #f8f8f2;
|
|
||||||
background: none;
|
|
||||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
|
||||||
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*="language-"] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
background: #282a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre) > code[class*="language-"] {
|
|
||||||
padding: 0.1em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: #6272a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespace {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.tag,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol,
|
|
||||||
.token.deleted {
|
|
||||||
color: #ff79c6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.boolean,
|
|
||||||
.token.number {
|
|
||||||
color: #bd93f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.builtin,
|
|
||||||
.token.inserted {
|
|
||||||
color: #50fa7b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url,
|
|
||||||
.language-css .token.string,
|
|
||||||
.style .token.string,
|
|
||||||
.token.variable {
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.atrule,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.function,
|
|
||||||
.token.class-name {
|
|
||||||
color: #f1fa8c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.keyword {
|
|
||||||
color: #8be9fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.regex,
|
|
||||||
.token.important {
|
|
||||||
color: #ffb86c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 88 KiB |