Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0ff062b21 | ||
|
|
ad722b09cf | ||
|
|
d78f23d7e2 | ||
|
|
0c68599d35 | ||
|
|
ca171b7c1b | ||
|
|
7aa70c83de | ||
|
|
585582244d | ||
|
|
1c3b3c1f22 | ||
|
|
db7a4f0b01 | ||
|
|
655dda8efb | ||
|
|
f09204ff5a | ||
|
|
817cd6f87c | ||
|
|
c55ac1df3b | ||
|
|
9ba3357a58 | ||
|
|
221287bbd5 | ||
|
|
3fea94f669 | ||
|
|
9a5c008a25 | ||
|
|
11e20e7f8c | ||
|
|
f0e46639d0 | ||
|
|
7d602e01a9 | ||
|
|
e6052152ea | ||
|
|
ba438b0640 | ||
|
|
4a7ee4dcf5 | ||
|
|
f1e528d3a7 | ||
|
|
1f573102d3 | ||
|
|
d5df706b47 | ||
|
|
6cf7da5557 | ||
|
|
84a29d7cda | ||
|
|
f1ec7c6495 | ||
|
|
6a7eabf5cb | ||
|
|
d84c11319d | ||
|
|
bc39dcdd72 | ||
|
|
1ba356a91f | ||
|
|
c5ba0f783a | ||
|
|
c508633262 | ||
|
|
1d7bb3e051 | ||
|
|
b875727753 | ||
|
|
1e5ec6028d | ||
|
|
5b6a69444e | ||
|
|
321007bb87 | ||
|
|
5f1c33d750 | ||
|
|
1af173cb24 | ||
|
|
8a3ccb6231 | ||
|
|
e868080a69 | ||
|
|
a7d12bafc0 | ||
|
|
f38d683f4e | ||
|
|
240c88220d | ||
|
|
f822a56993 | ||
|
|
fb88ba8a1f | ||
|
|
3ffd832640 | ||
|
|
59c5a9932d | ||
|
|
7c0003f823 | ||
|
|
a798a8a784 | ||
|
|
18c6b1e4ad | ||
|
|
4f03487a53 | ||
|
|
ef3820fad8 | ||
|
|
f0c635bd3b | ||
|
|
6d950c09e6 | ||
|
|
26bd845a72 | ||
|
|
96f7e32137 | ||
|
|
3c848b4ded | ||
|
|
f1a807ed46 | ||
|
|
ae2c56805c | ||
|
|
a2fcfa3b89 | ||
|
|
0eb6c559c6 | ||
|
|
bd5f34b7f9 | ||
|
|
042155ffdd | ||
|
|
08ef0a2437 | ||
|
|
e473268df1 | ||
|
|
e24fd6a33f | ||
|
|
eccee44866 | ||
|
|
51344ebe00 | ||
|
|
b3589257ae | ||
|
|
9c960b2a8e | ||
|
|
080a77031d | ||
|
|
71b37f690c | ||
|
|
7446cfc186 | ||
|
|
9fa0fe8e6c | ||
|
|
2dfb9bac2a | ||
|
|
2764219867 | ||
|
|
e68ab20420 | ||
|
|
c99f4b7633 | ||
|
|
335d3e33ba | ||
|
|
1045f0a689 | ||
|
|
5062769956 | ||
|
|
b791044a22 | ||
|
|
81ff2b89f1 | ||
|
|
ee576b9a69 | ||
|
|
d7ab912917 | ||
|
|
fc74ce30ed | ||
|
|
fad7c5221b | ||
|
|
3e4a4a3031 | ||
|
|
8863e4575c | ||
|
|
94a8112d45 | ||
|
|
7ee9b0c716 | ||
|
|
b97d3a55f2 | ||
|
|
a3b40f7f40 | ||
|
|
de5533d942 | ||
|
|
8022594ace | ||
|
|
ec20d03c50 | ||
|
|
bb6e431bd9 | ||
|
|
8c2303dec9 | ||
|
|
d0df8df108 | ||
|
|
a7cff0df66 | ||
|
|
cd2b7832d4 | ||
|
|
3919b24df8 | ||
|
|
fc25746c2d | ||
|
|
ab0999d7d7 | ||
|
|
c2b0e02b75 | ||
|
|
a6e746d27e | ||
|
|
a0896c9799 | ||
|
|
a87654e427 | ||
|
|
8ad7a8a9b0 | ||
|
|
9f3c72ec6d | ||
|
|
13acb1d56e | ||
|
|
d982a09870 | ||
|
|
1dee42dc79 | ||
|
|
e3c9b16de3 | ||
|
|
d657a6e262 | ||
|
|
e0c4584529 | ||
|
|
f812d5ab04 | ||
|
|
c1cec366fb | ||
|
|
3e300c66c3 | ||
|
|
f7335d9162 | ||
|
|
e7ed106a01 | ||
|
|
cf09dbe429 | ||
|
|
73a91df77e | ||
|
|
fc6912eb59 | ||
|
|
f646684f4d | ||
|
|
2f0e617b1a | ||
|
|
3e9c2f8e94 | ||
|
|
8689082c7b | ||
|
|
aa98d7dd2a | ||
|
|
a361ce52b5 | ||
|
|
594260ce5d | ||
|
|
53a240953f | ||
|
|
4c86477d6f | ||
|
|
e2235e7060 | ||
|
|
3dd49cd5d3 | ||
|
|
65bb811f15 | ||
|
|
ce60c0566f | ||
|
|
0ab4832349 | ||
|
|
5286de2f1e | ||
|
|
197d458c78 | ||
|
|
845229d629 | ||
|
|
30099f730f | ||
|
|
f26dace129 | ||
|
|
0a434f1add | ||
|
|
883f36b26d | ||
|
|
a7cd73b314 | ||
|
|
c19698bc41 | ||
|
|
0197d12119 | ||
|
|
038b13dd31 | ||
|
|
c7a6cfc897 | ||
|
|
a6912deb9f | ||
|
|
709029a6c6 | ||
|
|
060f3b5915 | ||
|
|
139afa2943 | ||
|
|
cef8ffc1a7 | ||
|
|
693b83e0c6 | ||
|
|
b35b035c09 | ||
|
|
b8614e73ff | ||
|
|
6bd52fe961 | ||
|
|
b370fd187c | ||
|
|
9333800002 | ||
|
|
b76d141dbc | ||
|
|
b66fbd617d | ||
|
|
802a201d82 | ||
|
|
508752691c | ||
|
|
484f1d6921 | ||
|
|
a27876b7a9 | ||
|
|
7c4f0bf67f | ||
|
|
41cc76d557 | ||
|
|
a6bac89a87 | ||
|
|
b7591c0b41 | ||
|
|
ffa8ff1d03 | ||
|
|
bac33c4689 | ||
|
|
bce27c1e39 | ||
|
|
5e1295677c | ||
|
|
51891e8a8a | ||
|
|
10993667ab | ||
|
|
fd62c2a8c4 | ||
|
|
85fbd4a104 | ||
|
|
5ebda4105d | ||
|
|
4c7907c106 | ||
|
|
beddf76198 | ||
|
|
d450d35935 | ||
|
|
11492fef24 | ||
|
|
74c0d07a76 | ||
|
|
dc769235a7 | ||
|
|
52cc029b01 | ||
|
|
39dc007513 | ||
|
|
ba1fe9a603 | ||
|
|
e1f8030009 | ||
|
|
9a392a71f6 | ||
|
|
dc0c78fee2 | ||
|
|
b3bd54ce81 | ||
|
|
ba21fa85d2 | ||
|
|
2ab35bb925 | ||
|
|
9cacad0120 | ||
|
|
f1fa859961 | ||
|
|
9a9fb24de8 | ||
|
|
3424666ec9 | ||
|
|
416bf7064c | ||
|
|
f5cf3acd3a | ||
|
|
975ffd2d84 | ||
|
|
145264719f | ||
|
|
a929376cb2 | ||
|
|
478049e23e | ||
|
|
631dee95a7 | ||
|
|
c4ff0b4107 | ||
|
|
bcd350584e | ||
|
|
050045f644 | ||
|
|
7e9440b45e | ||
|
|
cd9c0ac742 | ||
|
|
2d018c4967 | ||
|
|
f4d3cc6c8e | ||
|
|
cd6cece45e | ||
|
|
54b5b63f0e | ||
|
|
680f1b01ad | ||
|
|
078b0296f5 | ||
|
|
c956758a4a | ||
|
|
477120ef3b | ||
|
|
0ee95630ef | ||
|
|
fb0319a977 | ||
|
|
ea1a78abf5 | ||
|
|
3428e11b85 | ||
|
|
0e0771d0ec | ||
|
|
d78e2ad0b3 | ||
|
|
ae31da0b29 | ||
|
|
39febe759e | ||
|
|
06ee907199 | ||
|
|
f8c1ca5c56 | ||
|
|
6da58269bd | ||
|
|
4bf6c61bee | ||
|
|
a07c85a9cc | ||
|
|
95a9f12b68 | ||
|
|
252b0f3e15 | ||
|
|
ed268b32b3 | ||
|
|
e2319f2fda | ||
|
|
9ec69631f3 | ||
|
|
83437ffea7 | ||
|
|
be9846dc22 | ||
|
|
f071e0d6bc | ||
|
|
62a176d20c | ||
|
|
2f8ff36638 | ||
|
|
fe236e3c66 | ||
|
|
38e319a215 | ||
|
|
05057d06ad | ||
|
|
0b0b832130 | ||
|
|
413d3354c7 | ||
|
|
f1c7fff800 | ||
|
|
6fe90dea5b | ||
|
|
25ab2b0368 | ||
|
|
94973b1420 | ||
|
|
0930cd782a | ||
|
|
0733bba4bf | ||
|
|
bf623365da | ||
|
|
dc88ea9182 | ||
|
|
f411541a76 | ||
|
|
ca3badc783 | ||
|
|
d7328f576a | ||
|
|
eaf72e2b73 | ||
|
|
bd2c4fff5c | ||
|
|
3ca66cf309 | ||
|
|
44c91bc85c | ||
|
|
a75ae5e615 | ||
|
|
8193104853 | ||
|
|
11e07e87d4 | ||
|
|
7b8f29534b | ||
|
|
e4e56c7dbb | ||
|
|
8a79c28398 | ||
|
|
a7c4545dbf | ||
|
|
d93079f682 | ||
|
|
44dcdba10f | ||
|
|
6e2d395156 | ||
|
|
990aa31437 | ||
|
|
a73d203983 | ||
|
|
2a9fba7d27 | ||
|
|
e4be2bc2f3 | ||
|
|
389e00a5e0 | ||
|
|
2be560e69a | ||
|
|
6abe7c783e | ||
|
|
8319eae519 | ||
|
|
921d670f53 | ||
|
|
39a8d8d297 | ||
|
|
2d826c90a0 | ||
|
|
d513a50e27 | ||
|
|
878bb6c265 | ||
|
|
69f1968e88 | ||
|
|
3a0ee7d4d6 | ||
|
|
5dd671c98e | ||
|
|
75a7b9c78d | ||
|
|
47a3bace5b | ||
|
|
8966ebbd03 |
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: '🕷️ Bug report'
|
||||
description: 'report bugs'
|
||||
title: '[Bug]'
|
||||
labels:
|
||||
- 'bug'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Bug report
|
||||
Please fill in the following information to help us reproduce the bug:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: 'Please specify the version of ChatGPT you are using, a newer version may have fixed the bug you encountered.Check the [UPDATE_LOG](https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md) for more information.'
|
||||
placeholder: 'e.g. v0.1.0'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
Please describe the bug here,if possible, please provide a minimal example to reproduce the bug.The content of `~/.chatgpt/chatgpt.log` may be helpful if you encounter a crash.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: OS
|
||||
attributes:
|
||||
label: OS
|
||||
description: 'Please specify the OS you are using.'
|
||||
placeholder: 'e.g. Ubuntu 22.04'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: 'If you think your environment may be related to the problem, please describe it here.'
|
||||
37
.github/ISSUE_TEMPLATE/build_error_report.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: '❌ Build error report'
|
||||
description: 'report errors when building by yourself'
|
||||
title: '[Build Error]'
|
||||
labels:
|
||||
- 'build error'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to build from the source code with the latest version of ChatGPT.'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Build error report
|
||||
Please fill in the following information to help us reproduce the bug:
|
||||
- type: textarea
|
||||
id: error
|
||||
attributes:
|
||||
label: Error message
|
||||
description: 'Please paste the error message here.'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: OS
|
||||
attributes:
|
||||
label: OS
|
||||
description: 'Please specify the OS you are using.'
|
||||
placeholder: 'e.g. Ubuntu 22.04'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: 'If you think your environment may be related to the problem, please describe it here.'
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
19
.github/ISSUE_TEMPLATE/docmentation_issue.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: '📚 Documentation Issue'
|
||||
description: 'report documentation issues, typos welcome!'
|
||||
title: '[Doc]'
|
||||
labels:
|
||||
- 'documentation'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
|
||||
- type: textarea
|
||||
id: doc-description
|
||||
attributes:
|
||||
label: 'Provide a description of requested docs changes'
|
||||
description: 'Briefly describe the requested docs changes.'
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please limit one request per issue.
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: '⭐ Feature or enhancement request'
|
||||
description: 'suggest new features or enhancements'
|
||||
title: '[Feature]'
|
||||
labels:
|
||||
- 'enhancement'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: 'Feature description'
|
||||
description: "Describe the feature or enhancements you'd like to see."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: 'Motivation'
|
||||
description: 'Describe the motivation for this feature or enhancement.'
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 'Alternatives'
|
||||
description: "Describe any alternatives you've considered."
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 'Additional context'
|
||||
description: 'Add any other context or screenshots about the feature request here.'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please limit one request per issue.
|
||||
34
.github/ISSUE_TEMPLATE/security.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: '⚠️ Security&Privacy issue'
|
||||
description: 'Report security or privacy issues'
|
||||
title: '[Security]'
|
||||
labels:
|
||||
- 'security'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
|
||||
- type: textarea
|
||||
id: security-description
|
||||
attributes:
|
||||
label: 'Description'
|
||||
description: 'Describe the security or privacy issue.'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: 'Motivation'
|
||||
description: 'Describe the motivation for this security or privacy issue.'
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 'Alternatives'
|
||||
description: "Describe any alternatives you've considered."
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 'Additional context'
|
||||
description: 'Add any other context or screenshots about the security or privacy issue here.'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please limit one request per issue.
|
||||
68
.github/workflows/release.yml
vendored
@@ -2,13 +2,12 @@ name: Release CI
|
||||
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }}
|
||||
|
||||
@@ -19,7 +18,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "using version tag ${GITHUB_REF:10}"
|
||||
echo ::set-output name=version::"${GITHUB_REF:10}"
|
||||
echo "version=${GITHUB_REF:10}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
@@ -27,8 +26,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: '${{ steps.get_version.outputs.VERSION }}'
|
||||
release_name: 'ChatGPT ${{ steps.get_version.outputs.VERSION }}'
|
||||
tag_name: '${{ env.version }}'
|
||||
release_name: 'ChatGPT ${{ env.version }}'
|
||||
body: 'See the assets to download this version and install.'
|
||||
|
||||
build-tauri:
|
||||
@@ -36,47 +35,35 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-latest, windows-latest]
|
||||
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Rust cache
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
node-version: 16
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install app dependencies and build it
|
||||
run: yarn && yarn build:fe
|
||||
|
||||
# - name: Rewrite tauri.conf.json
|
||||
# run: yarn fix:conf
|
||||
|
||||
- name: fix tray icon
|
||||
if: matrix.platform != 'macos-latest'
|
||||
run: |
|
||||
yarn fix:tray
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -87,7 +74,7 @@ jobs:
|
||||
releaseId: ${{ needs.create-release.outputs.RELEASE_UPLOAD_ID }}
|
||||
|
||||
updater:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [create-release, build-tauri]
|
||||
|
||||
steps:
|
||||
@@ -102,3 +89,14 @@ jobs:
|
||||
# 📝: Edit the deployment directory
|
||||
publish_dir: ./updater
|
||||
force_orphan: true
|
||||
|
||||
# publish-winget:
|
||||
# # Action can only be run on windows
|
||||
# runs-on: windows-latest
|
||||
# needs: [create-release, build-tauri]
|
||||
# steps:
|
||||
# - uses: vedantmgoyal2009/winget-releaser@v1
|
||||
# with:
|
||||
# identifier: lencx.ChatGPT
|
||||
# token: ${{ secrets.WINGET_TOKEN }}
|
||||
# version: ${{ env.version }}
|
||||
|
||||
6
.husky/pre-commit
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run pretty-quick
|
||||
cargo fmt
|
||||
git add .
|
||||
45
.prettierignore
Normal file
@@ -0,0 +1,45 @@
|
||||
package-lock.json
|
||||
node_modules/
|
||||
yarn.lock
|
||||
*.lock
|
||||
|
||||
casks/
|
||||
|
||||
# rust
|
||||
src-tauri/
|
||||
target/
|
||||
Cargo.lock
|
||||
*.toml
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
assets/**
|
||||
public/**
|
||||
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.prettierignore
|
||||
|
||||
LICENSE
|
||||
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Awesome ChatGPT
|
||||
|
||||
- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) - This repo includes ChatGPT promt curation to use ChatGPT better.
|
||||
- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) - This repo includes ChatGPT prompt curation to use ChatGPT better.
|
||||
- [Awesome ChatGPT](https://github.com/humanloop/awesome-chatgpt) - Curated list of awesome tools, demos, docs for ChatGPT and GPT-3
|
||||
|
||||
## Extension
|
||||
|
||||
9
Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[workspace]
|
||||
members = ["src-tauri"]
|
||||
|
||||
# fix: mac v1.2.0 can not copy/paste
|
||||
# https://github.com/tauri-apps/tauri/issues/5669
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
178
README-ZH_CN.md
@@ -1,84 +1,105 @@
|
||||
<p align="center">
|
||||
<img width="180" src="./public/logo.png" alt="ChatGPT">
|
||||
<h1 align="center">ChatGPT</h1>
|
||||
<p align="center">ChatGPT 桌面应用(Mac, Windows and Linux)</p>
|
||||
</p>
|
||||
|
||||
> ChatGPT 桌面应用
|
||||
|
||||
[](./README.md)
|
||||
[](./README-ZH_CN.md)
|
||||
|
||||
[](./README-ZH_CN.md)\
|
||||

|
||||
[](https://github.com/lencx/ChatGPT/releases)
|
||||
[](https://discord.gg/aPhCRf4zZr)
|
||||
[](https://twitter.com/lencx_)
|
||||
[](https://twitter.com/lencx_)
|
||||
|
||||
[Awesome ChatGPT](./AWESOME.md)
|
||||
<!-- [](https://twitter.com/lencx_) -->
|
||||
|
||||
## 📦 下载
|
||||
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a>
|
||||
|
||||
[📝 更新日志](./UPDATE_LOG.md)
|
||||
## 📦 安装
|
||||
|
||||
- [📝 更新日志](./UPDATE_LOG.md)
|
||||
- [🕒 历史版本...](https://github.com/lencx/ChatGPT/releases)
|
||||
|
||||
<!-- download start -->
|
||||
|
||||
**最新版:**
|
||||
### Windows
|
||||
|
||||
- `Mac`: [ChatGPT_0.5.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/ChatGPT_0.5.0_x64.dmg)
|
||||
- `Linux`: [chat-gpt_0.5.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/chat-gpt_0.5.0_amd64.deb)
|
||||
- `Windows`: [ChatGPT_0.5.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/ChatGPT_0.5.0_x64_en-US.msi)
|
||||
- [ChatGPT_0.10.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64_en-US.msi):
|
||||
- 使用 [winget](https://winstall.app/apps/lencx.ChatGPT):
|
||||
|
||||
[其他版本...](https://github.com/lencx/ChatGPT/releases)
|
||||
```bash
|
||||
# install the latest version
|
||||
winget install --id=lencx.ChatGPT -e
|
||||
|
||||
# install the specified version
|
||||
winget install --id=lencx.ChatGPT -e --version 0.10.0
|
||||
```
|
||||
|
||||
**注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.10.1))**
|
||||
|
||||
### Mac
|
||||
|
||||
- [ChatGPT_0.10.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64.dmg)
|
||||
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT.app.tar.gz)
|
||||
- Homebrew \
|
||||
_[Homebrew 快捷安装](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||
```sh
|
||||
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||
brew install --cask chatgpt --no-quarantine
|
||||
```
|
||||
如果你坚持使用 _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_ ,则需要添加以下配置:
|
||||
```rb
|
||||
repo = "lencx/chatgpt"
|
||||
tap repo, "https://github.com/#{repo}.git"
|
||||
cask "chatgpt", args: { "no-quarantine": true }
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
- [chat-gpt_0.10.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.deb)
|
||||
- [chat-gpt_0.10.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它**
|
||||
- 使用 [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin):
|
||||
```bash
|
||||
yay -S chatgpt-desktop-bin
|
||||
```
|
||||
|
||||
<!-- download end -->
|
||||
|
||||
### brew 安装
|
||||
|
||||
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||
|
||||
~~~ sh
|
||||
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||
brew install --cask chatgpt --no-quarantine
|
||||
~~~
|
||||
|
||||
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
||||
|
||||
~~~ rb
|
||||
repo = "lencx/chatgpt"
|
||||
tap repo, "https://github.com/#{repo}.git"
|
||||
cask "popcorn-time", args: { "no-quarantine": true }
|
||||
~~~
|
||||
|
||||
## 📢 公告
|
||||
|
||||
这是一个令人兴奋的重大更新。像 `Telegram 机器人指令` 那样工作,帮助你快速填充自定模型,来让 ChatGPT 按照你想要的方式去工作。这个项目倾注了我大量业余时间,如果它对你有所帮助,宣传转发,或者 star 都是对我的巨大鼓励。我希望我可以持续更新下去,加入更多有趣的功能。
|
||||
|
||||
### 如何使用指令?
|
||||
|
||||
你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。
|
||||
你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。也可以使用 `Sync Prompts`,来一键同步所有,如果你不想让某些提示出现在你的斜杠命令,你可以禁用它们。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
|
||||
<!-- 数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。 -->
|
||||
|
||||
项目会维护一份常用命令,您也可以直接将 [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) 复制到你的本地目录 `~/.chatgpt/chat.model.json`。
|
||||
|
||||
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。
|
||||
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。斜杠命令下,使用 TAB 键修改 `{q}` 标签内容(仅支持单个修改 [#54](https://github.com/lencx/ChatGPT/issues/54))。使用键盘 `⇧` 和 `⇩`(上下键)来选择斜杠指令。
|
||||
|
||||

|
||||

|
||||
|
||||
## ✨ 功能概览
|
||||
|
||||
- 跨平台: `macOS` `Linux` `Windows`
|
||||
- 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接)
|
||||
- 主窗口和系统托盘支持自定义 URL,将任意网站包装成一个桌面应用
|
||||
- 应用自动升级通知
|
||||
- 丰富的快捷键
|
||||
- 系统托盘悬浮窗
|
||||
- 应用菜单功能强大
|
||||
- 支持斜杠命令及其配置(可手动配置或从文件同步 [#55](https://github.com/lencx/ChatGPT/issues/55))
|
||||
- 自定义全局快捷键 ([#108](https://github.com/lencx/ChatGPT/issues/108))
|
||||
- 划词搜索 ([#122](https://github.com/lencx/ChatGPT/issues/122) 鼠标选中文本,不超过 400 个字符):应用使用 Tauri 构建,因其安全限制,会导致部分操作按钮无效,建议前往浏览器操作。
|
||||
|
||||
### 菜单项
|
||||
### #️⃣ 菜单项
|
||||
|
||||
- **Preferences (喜好)**
|
||||
- `Theme` - `Light`, `Dark` (仅支持 macOS 和 Windows)
|
||||
- `Theme` - `Light`, `Dark`, `System` (仅支持 macOS 和 Windows)
|
||||
- `Stay On Top`: 窗口置顶
|
||||
- `Titlebar`: 是否显示 `Titlebar`,仅 macOS 支持
|
||||
- `Inject Script`: 用于修改网站的用户自定义脚本
|
||||
@@ -99,18 +120,65 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
||||
- `Report Bug`: 报告 BUG 或反馈建议
|
||||
- `Toggle Developer Tools`: 网站调试工具,调试页面或脚本可能需要
|
||||
|
||||
## ⚙️ 应用配置
|
||||
|
||||
| 平台 | 路径 |
|
||||
| ------- | ------------------------- |
|
||||
| Linux | `/home/lencx/.chatgpt` |
|
||||
| macOS | `/Users/lencx/.chatgpt` |
|
||||
| Windows | `C:\Users\lencx\.chatgpt` |
|
||||
|
||||
- `[.chatgpt]` - 应用配置根路径
|
||||
- `chat.conf.json` - 应用喜好配置
|
||||
- `chat.awesome.json` - 自定义 URL 列表,类似于浏览器书签。可以将任意 URL 作为主窗口或托盘窗口 (**Control Conter -> Awesome**)
|
||||
- `chat.model.json` - ChatGPT 输入提示,通过斜杠命令来快速完成输入,主要包含三部分:
|
||||
- `user_custom` - 需要手动录入 (**Control Conter -> Language Model -> User Custom**)
|
||||
- `sync_prompts` - 从 [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 同步数据 (**Control Conter -> Language Model -> Sync Prompts**)
|
||||
- `sync_custom` - 同步自定义的 json 或 csv 文件数据,支持本地和远程 (**Control Conter -> Language Model -> Sync Custom**)
|
||||
- `chat.model.cmd.json` - 过滤(是否启用)和排序处理后的斜杠命令数据
|
||||
- `[cache_model]` - 缓存同步或录入的数据
|
||||
- `chatgpt_prompts.json` - 缓存 `sync_prompts` 数据
|
||||
- `user_custom.json` - 缓存 `user_custom` 数据
|
||||
- `ae6cf32a6f8541b499d6bfe549dbfca3.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||
- `4f695d3cfbf8491e9b1f3fab6d85715c.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||
- `bd1b96f15a1644f7bd647cc53073ff8f.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||
|
||||
### 客户端信息同步
|
||||
|
||||
目前同步自定文件仅支持 json 和 csv,且需要满足以下格式,否则会导致应用异常:
|
||||
|
||||
`JSON 格式`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"cmd": "a",
|
||||
"act": "aa",
|
||||
"prompt": "aaa aaa aaa"
|
||||
},
|
||||
{
|
||||
"cmd": "b",
|
||||
"act": "bb",
|
||||
"prompt": "bbb bbb bbb"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`CSV 格式`
|
||||
|
||||
```csv
|
||||
"cmd","act","prompt"
|
||||
"a","aa","aaa aaa aaa"
|
||||
"b","bb","bbb bbb bbb"
|
||||
```
|
||||
|
||||
## 👀 预览
|
||||
|
||||
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
|
||||
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
|
||||
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/chatgpt-control-center-general.png" alt="control center">
|
||||
<img width="320" src="./assets/chatgpt-export.png" alt="export"> <img width="320" src="./assets/chatgpt-dalle2-tray.png" alt="dalle2 tray">
|
||||
<img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||
|
||||
|
||||
---
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
## ❓常见问题
|
||||
## ❓ 常见问题
|
||||
|
||||
### 不能打开 ChatGPT
|
||||
|
||||
@@ -124,18 +192,21 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
||||
|
||||
它是安全的,仅仅只是对 [OpenAI ChatGPT](https://chat.openai.com) 网站的包装,注入了一些额外功能(均在本地,未发起网络请求),如果存疑,可以检查源代码。
|
||||
|
||||
### Developer cannot be verified?
|
||||
### 开发者未验证?
|
||||
|
||||
Mac 上无法安装,提示开发者未验证,具体可以查看下面给出的解决方案(它是开源的,很安全)。
|
||||
|
||||
- [Open a Mac app from an unidentified developer](https://support.apple.com/en-sg/guide/mac-help/mh40616/mac)
|
||||
|
||||
---
|
||||
|
||||
### 我想自己构建它?
|
||||
|
||||
#### 预安装
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Rust (必须)](https://www.rust-lang.org/)
|
||||
- [Node.js (必须)](https://nodejs.org/)
|
||||
- [VS Code (可选)](https://code.visualstudio.com/)
|
||||
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
|
||||
|
||||
@@ -159,6 +230,9 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
|
||||
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
|
||||
|
||||
## ❤️ 感谢
|
||||
|
||||
- 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改
|
||||
@@ -166,7 +240,7 @@ yarn build
|
||||
|
||||
---
|
||||
|
||||
[](https://star-history.com/#lencx/chatgpt&Date)
|
||||
[](https://star-history.com/#lencx/chatgpt&Timeline)
|
||||
|
||||
## 中国用户
|
||||
|
||||
|
||||
188
README.md
@@ -1,86 +1,108 @@
|
||||
<p align="center">
|
||||
<img width="180" src="./public/logo.png" alt="ChatGPT">
|
||||
<h1 align="center">ChatGPT</h1>
|
||||
<p align="center">ChatGPT Desktop Application (Mac, Windows and Linux)</p>
|
||||
</p>
|
||||
|
||||
> ChatGPT Desktop Application
|
||||
|
||||
[](./README.md)
|
||||
[](./README-ZH_CN.md)
|
||||
|
||||
[](./README-ZH_CN.md)\
|
||||

|
||||
[](https://github.com/lencx/ChatGPT/releases)
|
||||
[](https://discord.gg/aPhCRf4zZr)
|
||||
[](https://twitter.com/lencx_)
|
||||
[](https://twitter.com/lencx_)
|
||||
|
||||
<!-- [](https://twitter.com/lencx_) -->
|
||||
|
||||
<!-- [](./README-ZH.md) -->
|
||||
|
||||
[Awesome ChatGPT](./AWESOME.md)
|
||||
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a>
|
||||
|
||||
## 📦 Downloads
|
||||
## 📦 Install
|
||||
|
||||
[📝 Update Log](./UPDATE_LOG.md)
|
||||
- [📝 Update Log](./UPDATE_LOG.md)
|
||||
- [🕒 History versions...](https://github.com/lencx/ChatGPT/releases)
|
||||
|
||||
<!-- download start -->
|
||||
|
||||
**Latest:**
|
||||
### Windows
|
||||
|
||||
- `Mac`: [ChatGPT_0.5.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/ChatGPT_0.5.0_x64.dmg)
|
||||
- `Linux`: [chat-gpt_0.5.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/chat-gpt_0.5.0_amd64.deb)
|
||||
- `Windows`: [ChatGPT_0.5.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.5.0/ChatGPT_0.5.0_x64_en-US.msi)
|
||||
- [ChatGPT_0.10.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64_en-US.msi): Direct download installer
|
||||
- Use [winget](https://winstall.app/apps/lencx.ChatGPT):
|
||||
|
||||
[Other version...](https://github.com/lencx/ChatGPT/releases)
|
||||
```bash
|
||||
# install the latest version
|
||||
winget install --id=lencx.ChatGPT -e
|
||||
|
||||
# install the specified version
|
||||
winget install --id=lencx.ChatGPT -e --version 0.10.0
|
||||
```
|
||||
|
||||
**Note: If the installation path and application name are the same, it will lead to conflict ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.10.1))**
|
||||
|
||||
### Mac
|
||||
|
||||
- [ChatGPT_0.10.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64.dmg): Direct download installer
|
||||
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT.app.tar.gz): Download the `.app` installer
|
||||
- Homebrew \
|
||||
Or you can install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||
```sh
|
||||
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||
brew install --cask chatgpt --no-quarantine
|
||||
```
|
||||
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
||||
```rb
|
||||
repo = "lencx/chatgpt"
|
||||
tap repo, "https://github.com/#{repo}.git"
|
||||
cask "chatgpt", args: { "no-quarantine": true }
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
- [chat-gpt_0.10.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility
|
||||
- [chat-gpt_0.10.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run
|
||||
- Available on [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin) with the package name `chatgpt-desktop-bin`, and you can use your favourite AUR package manager to install it.
|
||||
|
||||
<!-- download end -->
|
||||
|
||||
### Install
|
||||
|
||||
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||
|
||||
~~~ sh
|
||||
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||
brew install --cask chatgpt --no-quarantine
|
||||
~~~
|
||||
|
||||
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
||||
|
||||
~~~ rb
|
||||
repo = "lencx/chatgpt"
|
||||
tap repo, "https://github.com/#{repo}.git"
|
||||
cask "popcorn-time", args: { "no-quarantine": true }
|
||||
~~~
|
||||
|
||||
## 📢 Announcement
|
||||
|
||||
### ChatGPT Prompts!
|
||||
|
||||
This is a major and exciting update. It works like a `Telegram bot command` and helps you quickly populate custom models to make chatgpt work the way you want it to. This project has taken a lot of my spare time, so if it helps you, please help spread the word or star it would be a great encouragement to me. I hope I can keep updating it and adding more interesting features.
|
||||
|
||||
### How does it work?
|
||||
|
||||
You can look at [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) to find interesting features to import into the app.
|
||||
You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)** to find interesting features to import into the app. You can also use `Sync Prompts` to sync all in one click, and if you don't want certain prompts to appear in your slash commands, you can disable them.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
|
||||
<!-- After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`). -->
|
||||
|
||||
The project maintains a list of common commands, or you can copy [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) directly to your local directory `~/.chatgpt/chat.model.json`
|
||||
|
||||
In the chatgpt text input area, type a character starting with `/` to bring up the command prompt, press the spacebar, and it will fill the input area with the text associated with the command by default (note: if it contains multiple command prompts, it will only select the first one as the fill, you can keep typing until the first prompted command is the one you want, then press the spacebar. Or use the mouse to click on one of the multiple commands). When the fill is complete, you simply press the Enter key.
|
||||
- In the chatgpt text input area, type a character starting with `/` to bring up the command prompt, press the spacebar, and it will fill the input area with the text associated with the command by default (note: if it contains multiple command prompts, it will only select the first one as the fill, you can keep typing until the first prompted command is the one you want, then press the spacebar.
|
||||
- Or use the mouse to click on one of the multiple commands). When the fill is complete, you simply press the Enter key.
|
||||
- Under the slash command, use the tab key to modify the contents of the `{q}` tag (only single changes are supported [#54](https://github.com/lencx/ChatGPT/issues/54)). Use the keyboard `⇧` (arrow up) and `⇩` (arrow down) keys to select the slash command.
|
||||
|
||||

|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Multi-platform: `macOS` `Linux` `Windows`
|
||||
- Export ChatGPT history (PNG, PDF and Share Link)
|
||||
- Export ChatGPT history (PNG, PDF and Markdown)
|
||||
- The main window and system tray support custom URLs to wrap any website into a desktop application
|
||||
- Automatic application upgrade notification
|
||||
- Common shortcut keys
|
||||
- System tray hover window
|
||||
- Powerful menu items
|
||||
- Shortcut command typing chatgpt prompt
|
||||
- Support for slash commands and their configuration (can be configured manually or synchronized from a file [#55](https://github.com/lencx/ChatGPT/issues/55))
|
||||
- Customize global shortcuts ([#108](https://github.com/lencx/ChatGPT/issues/108))
|
||||
- Pop-up Search ([#122](https://github.com/lencx/ChatGPT/issues/122) mouse selected content, no more than 400 characters): The application is built using Tauri, and due to its security restrictions, some of the action buttons will not work, so we recommend going to your browser.
|
||||
|
||||
### MenuItem
|
||||
## #️⃣ MenuItem
|
||||
|
||||
- **Preferences**
|
||||
- `Theme` - `Light`, `Dark` (Only macOS and Windows are supported).
|
||||
- `Theme` - `Light`, `Dark`, `System` (Only macOS and Windows are supported).
|
||||
- `Stay On Top`: The window is stay on top of other windows.
|
||||
- `Titlebar`: Whether to display the titlebar, supported by macOS only.
|
||||
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): Hide application icons from the Dock(support macOS only).
|
||||
@@ -101,21 +123,71 @@ In the chatgpt text input area, type a character starting with `/` to bring up t
|
||||
- `Report Bug`: Report a bug or give feedback.
|
||||
- `Toggle Developer Tools`: Developer debugging tools.
|
||||
|
||||
## TODO
|
||||
## ⚙️ Application Configuration
|
||||
|
||||
- Web access capability ([#20](https://github.com/lencx/ChatGPT/issues/20))
|
||||
- `Control Center` - Feature Enhancements
|
||||
| Platform | Path |
|
||||
| -------- | ------------------------- |
|
||||
| Linux | `/home/lencx/.chatgpt` |
|
||||
| macOS | `/Users/lencx/.chatgpt` |
|
||||
| Windows | `C:\Users\lencx\.chatgpt` |
|
||||
|
||||
- `[.chatgpt]` - application configuration root folder
|
||||
- `chat.conf.json` - preferences configuration
|
||||
- `chat.awesome.json` - Custom URL lists, similar to browser bookmarks. Any URL can be used as the main window or tray window (**Control Conter -> Awesome**)
|
||||
- `chat.model.json` - prompts configuration,contains three parts:
|
||||
- `user_custom` - Requires manual data entry (**Control Conter -> Language Model -> User Custom**)
|
||||
- `sync_prompts` - Synchronizing data from [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) (**Control Conter -> Language Model -> Sync Prompts**)
|
||||
- `sync_custom` - Synchronize custom json and csv file data, support local and remote (**Control Conter -> Language Model -> Sync Custom**)
|
||||
- `chat.model.cmd.json` - filtered (whether to enable) and sorted slash commands
|
||||
- `[cache_model]` - caching model data
|
||||
- `chatgpt_prompts.json` - Cache `sync_prompts` data
|
||||
- `user_custom.json` - Cache `user_custom` data
|
||||
- `ae6cf32a6f8541b499d6bfe549dbfca3.json` - Randomly generated file names, cache `sync_custom` data
|
||||
- `4f695d3cfbf8491e9b1f3fab6d85715c.json` - Randomly generated file names, cache `sync_custom` data
|
||||
- `bd1b96f15a1644f7bd647cc53073ff8f.json` - Randomly generated file names, cache `sync_custom` data
|
||||
|
||||
### Sync Custom
|
||||
|
||||
Currently, only json and csv are supported for synchronizing custom files, and the following formats need to be met, otherwise the application will be abnormal:
|
||||
|
||||
`JSON format:`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"cmd": "a",
|
||||
"act": "aa",
|
||||
"prompt": "aaa aaa aaa"
|
||||
},
|
||||
{
|
||||
"cmd": "b",
|
||||
"act": "bb",
|
||||
"prompt": "bbb bbb bbb"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`CSV format`
|
||||
|
||||
```csv
|
||||
"cmd","act","prompt"
|
||||
"a","aa","aaa aaa aaa"
|
||||
"b","bb","bbb bbb bbb"
|
||||
```
|
||||
|
||||
## 📌 TODO
|
||||
|
||||
<!-- - Web access capability ([#20](https://github.com/lencx/ChatGPT/issues/20)) -->
|
||||
|
||||
- `Control Center` enhancement
|
||||
- `Pop-up Search` enhancement
|
||||
- ...
|
||||
|
||||
## 👀 Preview
|
||||
|
||||
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
|
||||
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
|
||||
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||
|
||||
---
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/chatgpt-popup-search.png" alt="popup search">
|
||||
<img width="320" src="./assets/chatgpt-control-center-general.png" alt="control center"> <img width="320" src="./assets/chatgpt-export.png" alt="export">
|
||||
<img width="320" src="./assets/chatgpt-dalle2-tray.png" alt="dalle2 tray"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||
|
||||
## ❓FAQ
|
||||
|
||||
@@ -135,12 +207,15 @@ It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website,
|
||||
|
||||
- [Open a Mac app from an unidentified developer](https://support.apple.com/en-sg/guide/mac-help/mh40616/mac)
|
||||
|
||||
### How do i build it?
|
||||
---
|
||||
|
||||
### How do I build it?
|
||||
|
||||
#### PreInstall
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Rust (Required)](https://www.rust-lang.org/)
|
||||
- [Node.js (Required)](https://nodejs.org/)
|
||||
- [VS Code (Optional)](https://code.visualstudio.com/)
|
||||
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
|
||||
|
||||
@@ -164,6 +239,9 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
|
||||
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
|
||||
|
||||
## ❤️ Thanks
|
||||
|
||||
- The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications.
|
||||
@@ -171,7 +249,7 @@ yarn build
|
||||
|
||||
---
|
||||
|
||||
[](https://star-history.com/#lencx/chatgpt&Date)
|
||||
[](https://star-history.com/#lencx/chatgpt&Timeline)
|
||||
|
||||
## 中国用户
|
||||
|
||||
|
||||
199
UPDATE_LOG.md
@@ -1,85 +1,220 @@
|
||||
# UPDATE LOG
|
||||
|
||||
## v0.10.1
|
||||
|
||||
Fix:
|
||||
|
||||
- Program exception when `Awesome` data is empty (https://github.com/lencx/ChatGPT/issues/248)
|
||||
|
||||
Feat:
|
||||
|
||||
- New shortcut key to change zoom level (30% - 200%), `+` or `-` 10% each time, `0` will be reset to 100% (https://github.com/lencx/ChatGPT/issues/202)
|
||||
- Windows: `Ctrl +`, `Ctrl -`, `Ctrl 0`
|
||||
- MacOS: `Cmd +`, `Cmd -`, `Cmd 0`
|
||||
|
||||
## v0.10.0
|
||||
|
||||
Fix:
|
||||
|
||||
- After exporting a file in Windows, open an empty file explorer (https://github.com/lencx/ChatGPT/issues/242)
|
||||
|
||||
Feat:
|
||||
|
||||
- Markdown files support editing and live preview
|
||||
- Add `Awesome` menu to the `Control Center` (similar to bookmarks, but it's just a start, more possibilities in the future), custom URL support for the home and tray windows (if you're tired of ChatGPT as your home screen).
|
||||
|
||||
## v0.9.2
|
||||
|
||||
Fix: Slash command does not work
|
||||
|
||||
## v0.9.1
|
||||
|
||||
Fix: Slash command does not work
|
||||
|
||||
## v0.9.0
|
||||
|
||||
Fix:
|
||||
|
||||
- Export button does not work
|
||||
|
||||
Feat:
|
||||
|
||||
- Add an export markdown button
|
||||
- `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews.
|
||||
|
||||
## v0.8.1
|
||||
|
||||
Fix:
|
||||
|
||||
- Export button keeps blinking
|
||||
- Export button in the old chat does not work
|
||||
- Disable export sharing links because it is a security risk
|
||||
|
||||
## v0.8.0
|
||||
|
||||
Feat:
|
||||
|
||||
- Theme enhancement (Light, Dark, System)
|
||||
- Automatic updates support `silent` settings
|
||||
- Pop-up search: select the ChatGPT content with the mouse, the `DALL·E 2` button appears, and click to jump (note: because the search content filled by the script cannot trigger the event directly, you need to enter a space in the input box to make the button clickable).
|
||||
|
||||
Fix:
|
||||
|
||||
- Close the main window and hide it in the tray (windows systems)
|
||||
|
||||
## v0.7.4
|
||||
|
||||
Fix:
|
||||
|
||||
- Trying to resolve linux errors: `error while loading shared libraries`
|
||||
- Customize global shortcuts (`Menu -> Preferences -> Control Center -> General -> Global Shortcut`)
|
||||
|
||||
## v0.7.3
|
||||
|
||||
Chore:
|
||||
|
||||
- Optimize slash command style
|
||||
- Optimize tray menu icon and button icons
|
||||
- Global shortcuts to the chatgpt app (mac: `Command + Shift + O`, windows: `Ctrl + Shift + O`)
|
||||
|
||||
## v0.7.2
|
||||
|
||||
Fix: Some windows systems cannot start the application
|
||||
|
||||
## v0.7.1
|
||||
|
||||
Fix:
|
||||
|
||||
- Some windows systems cannot start the application
|
||||
- Windows and linux add about menu (show version information)
|
||||
- The tray icon is indistinguishable from the background in dark mode on window and linux
|
||||
|
||||
## v0.7.0
|
||||
|
||||
Fix:
|
||||
|
||||
- Mac m1 copy/paste does not work on some system versions
|
||||
- Optimize the save chat log button to a small icon, the tray window no longer provides a save chat log button (the buttons causes the input area to become larger and the content area to become smaller)
|
||||
|
||||
Feat:
|
||||
|
||||
- Use the keyboard `⇧` (arrow up) and `⇩` (arrow down) keys to select the slash command
|
||||
<!-- - global shortcuts to the chatgpt app (mac: command+shift+o, windows: ctrl+shift+o) -->
|
||||
|
||||
## v0.6.10
|
||||
|
||||
Fix: Sync failure on windows
|
||||
|
||||
## v0.6.4
|
||||
|
||||
Fix: Path not allowed on the configured scope
|
||||
|
||||
Feat:
|
||||
|
||||
- Optimize the generated pdf file size
|
||||
- Menu added `Sync Prompts`
|
||||
- `Control Center` added `Sync Custom`
|
||||
- The slash command is triggered by the enter key
|
||||
- Under the slash command, use the tab key to modify the contents of the `{q}` tag (only single changes are supported (https://github.com/lencx/ChatGPT/issues/54)
|
||||
|
||||
## v0.6.0
|
||||
|
||||
Fix:
|
||||
|
||||
- Windows show Chinese when upgrading
|
||||
|
||||
## v0.5.1
|
||||
|
||||
Some optimization
|
||||
|
||||
## v0.5.0
|
||||
|
||||
feat: `Control Center` added `chatgpt-prompts` synchronization
|
||||
Feat: `Control Center` added `chatgpt-prompts` synchronization
|
||||
|
||||
## v0.4.2
|
||||
|
||||
add chatgpt log (path: `~/.chatgpt/chatgpt.log`)
|
||||
Add chatgpt log (path: `~/.chatgpt/chatgpt.log`)
|
||||
|
||||
## v0.4.1
|
||||
|
||||
fix:
|
||||
- tray window style optimization
|
||||
Fix:
|
||||
|
||||
- Tray window style optimization
|
||||
|
||||
## v0.4.0
|
||||
|
||||
feat:
|
||||
- customize the ChatGPT prompts command (https://github.com/lencx/ChatGPT#-announcement)
|
||||
- menu enhancement: hide application icons from the Dock (support macOS only)
|
||||
Feat:
|
||||
|
||||
- Customize the ChatGPT prompts command (https://github.com/lencx/ChatGPT#-announcement)
|
||||
- Menu enhancement: hide application icons from the Dock (support macOS only)
|
||||
|
||||
## v0.3.0
|
||||
|
||||
fix: can't open ChatGPT
|
||||
Fix: Can't open ChatGPT
|
||||
|
||||
feat: menu enhancement
|
||||
- the control center of ChatGPT application
|
||||
- open the configuration file directory
|
||||
Feat: Menu enhancement
|
||||
|
||||
- The control center of ChatGPT application
|
||||
- Open the configuration file directory
|
||||
|
||||
## v0.2.2
|
||||
|
||||
feat:
|
||||
- menu: go to config
|
||||
Feat:
|
||||
|
||||
- Menu: go to config
|
||||
|
||||
## v0.2.1
|
||||
|
||||
feat: menu optimization
|
||||
Feat: Menu optimization
|
||||
|
||||
## v0.2.0
|
||||
|
||||
feat: menu enhancement
|
||||
- customize user-agent to prevent security detection interception
|
||||
- clear all chatgpt configuration files
|
||||
Feat: Menu enhancement
|
||||
|
||||
- Customize user-agent to prevent security detection interception
|
||||
- Clear all chatgpt configuration files
|
||||
|
||||
## v0.1.8
|
||||
|
||||
feat:
|
||||
- menu enhancement: theme, titlebar
|
||||
- modify website address
|
||||
Feat:
|
||||
|
||||
- Menu enhancement: theme, titlebar
|
||||
- Modify website address
|
||||
|
||||
## v0.1.7
|
||||
|
||||
feat: tray window
|
||||
Feat: Tray window
|
||||
|
||||
## v0.1.6
|
||||
|
||||
feat:
|
||||
- stay on top
|
||||
- export ChatGPT history
|
||||
Feat:
|
||||
|
||||
- Stay on top
|
||||
- Export ChatGPT history
|
||||
|
||||
## v0.1.5
|
||||
|
||||
fix: mac can't use shortcut keys
|
||||
Fix: Mac can't use shortcut keys
|
||||
|
||||
## v0.1.4
|
||||
|
||||
feat:
|
||||
- beautify icons
|
||||
- add system tray menu
|
||||
Feat:
|
||||
|
||||
- Beautify icons
|
||||
- Add system tray menu
|
||||
|
||||
## v0.1.3
|
||||
|
||||
fix: only mac supports `TitleBarStyle`
|
||||
Fix: Only mac supports `TitleBarStyle`
|
||||
|
||||
## v0.1.2
|
||||
|
||||
initialization
|
||||
Initialization
|
||||
|
||||
## v0.1.1
|
||||
|
||||
initialization
|
||||
Initialization
|
||||
|
||||
## v0.1.0
|
||||
|
||||
initialization
|
||||
Initialization
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
BIN
assets/chatgpt-cmd.gif
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
assets/chatgpt-cmd.png
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
assets/chatgpt-control-center-general.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
BIN
assets/chatgpt-dalle2-tray.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
assets/chatgpt-export.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
assets/chatgpt-popup-search.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
assets/chatgpt-sync-prompts.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 500 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 989 KiB |
BIN
assets/tray.png
|
Before Width: | Height: | Size: 543 KiB |
@@ -1,6 +1,6 @@
|
||||
cask "chatgpt" do
|
||||
version "0.1.7"
|
||||
sha256 "1320b30a67e2506f9b45ffd2a48243d6141171c231dd698994ae5156a637eb3f"
|
||||
version "0.6.10"
|
||||
sha256 "e85062565f826d32219c53b184d6df9c89441d4231cdfff775c2de8c50ac9906"
|
||||
|
||||
url "https://github.com/lencx/ChatGPT/releases/download/v#{version}/ChatGPT_#{version}_x64.dmg"
|
||||
name "ChatGPT"
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"name": "ChatGPT Model",
|
||||
"link": "https://github.com/lencx/ChatGPT/blob/main/chat.model.md",
|
||||
"data": [
|
||||
{
|
||||
"cmd": "cfm",
|
||||
"act": "‘Character’ from ‘Movie/Book/Anything’",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act like {Character} from {series}. I want you to respond and answer like {Character}. do not write any explanations. only answer like {character}. You must know all of the knowledge of {character}. My first sentence is “Hi Character”"
|
||||
},
|
||||
{
|
||||
"cmd": "pc",
|
||||
"act": "Plagiarism Checker",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as a plagiarism checker. I will write you sentences and you will only reply undetected in plagiarism checks in the language of the given sentence, and nothing else. Do not write explanations on replies. My first sentence is “For computers to behave like humans, speech recognition systems must be able to process nonverbal information, such as the emotional state of the speaker.”"
|
||||
},
|
||||
{
|
||||
"cmd": "drunk",
|
||||
"act": "Drunk",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "Let’s play a game. You will role play as if you were drunk. Make your answer’s incoherent."
|
||||
},
|
||||
{
|
||||
"cmd": "tg",
|
||||
"act": "Travel Guide",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location. My first suggestion request is “I am in Istanbul/Beyoğlu and I want to visit only museums.”"
|
||||
},
|
||||
{
|
||||
"cmd": "eph",
|
||||
"act": "English Pronunciation Helper",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as an English pronunciation assistant for Turkish speaking people. I will write you sentences and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentence but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is “how the weather is in Istanbul?”"
|
||||
},
|
||||
{
|
||||
"cmd": "excel",
|
||||
"act": "Excel Sheet",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as a text based excel. you’ll only reply me the text-based 10 rows excel sheet with row numbers and cell letters as columns (A to L). First column header should be empty to reference row number. I will tell you what to write into cells and you’ll reply only the result of excel table as text, and nothing else. Do not write explanations. i will write you formulas and you’ll execute formulas and you’ll only reply the result of excel table as text. First, reply me the empty sheet."
|
||||
},
|
||||
{
|
||||
"cmd": "console",
|
||||
"act": "JavaScript Console",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log(“Hello World”);"
|
||||
},
|
||||
{
|
||||
"cmd": "pi",
|
||||
"act": "position Interviewer",
|
||||
"tags": [],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the position position. I want you to only reply as the interviewer. Do not write all the conservation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers. My first sentence is “Hi”"
|
||||
},
|
||||
{
|
||||
"cmd": "trans",
|
||||
"act": "English Translator and Improver",
|
||||
"tags": [
|
||||
"tools",
|
||||
"cx",
|
||||
"x"
|
||||
],
|
||||
"enable": true,
|
||||
"prompt": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is \"istanbulu cok seviyom burada olmak cok guzel\""
|
||||
},
|
||||
{
|
||||
"cmd": "terminal",
|
||||
"act": "Linux Terminal",
|
||||
"tags": [
|
||||
"dev"
|
||||
],
|
||||
"enable": true,
|
||||
"prompt": "i want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwd"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
package.json
@@ -8,9 +8,16 @@
|
||||
"build": "yarn tauri build",
|
||||
"updater": "tr updater",
|
||||
"release": "tr release --git",
|
||||
"fix:conf": "tr override --json.tauri_updater_active=false",
|
||||
"fix:tray": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon-light.png\" --json.tauri_systemTray_iconAsTemplate=false",
|
||||
"fix:tray:mac": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon.png\" --json.tauri_systemTray_iconAsTemplate=true",
|
||||
"download": "node ./scripts/download.js",
|
||||
"fmt:rs": "cargo fmt",
|
||||
"tr": "tr",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"prettier": "prettier -c --write '**/*.{js,md,ts,tsx,yml}'",
|
||||
"pretty-quick": "pretty-quick --staged",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "lencx <cxin1314@gmail.com>",
|
||||
@@ -31,13 +38,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"antd": "^5.0.6",
|
||||
"antd": "^5.1.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"github-markdown-css": "^5.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-resizable-panels": "^0.0.33",
|
||||
"react-router-dom": "^6.4.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"remark-comment-config": "^7.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -47,8 +63,12 @@
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"prettier": "^2.8.3",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.56.2",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.0",
|
||||
|
||||
14
rustfmt.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2021"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
# imports_granularity = "Crate"
|
||||
6
scripts/download.js
vendored
@@ -13,7 +13,9 @@ async function rewrite(filename) {
|
||||
flag = true;
|
||||
}
|
||||
if (flag) {
|
||||
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
|
||||
if (!/winget install --id=lencx.ChatGPT -e --version/.test(content[i])) {
|
||||
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
|
||||
}
|
||||
}
|
||||
if (endRe.test(content[i])) {
|
||||
break;
|
||||
@@ -28,4 +30,4 @@ async function init() {
|
||||
rewrite('README-ZH_CN.md');
|
||||
}
|
||||
|
||||
init().catch(console.error);
|
||||
init().catch(console.error);
|
||||
|
||||
@@ -16,17 +16,22 @@ tauri-build = {version = "1.2.1", features = [] }
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2.2", features = ["api-all", "devtools", "system-tray", "updater"] }
|
||||
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
||||
log = "0.4.17"
|
||||
csv = "1.1.6"
|
||||
thiserror = "1.0.38"
|
||||
walkdir = "2.3.2"
|
||||
regex = "1.7.0"
|
||||
reqwest = "0.11.13"
|
||||
wry = "0.24.1"
|
||||
dark-light = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.23.0", features = ["macros"] }
|
||||
tauri = { version = "1.2.4", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
|
||||
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
||||
tauri-plugin-log = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev", features = ["colored"] }
|
||||
tauri-plugin-autostart = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev" }
|
||||
|
||||
[dependencies.tauri-plugin-log]
|
||||
git = "https://github.com/tauri-apps/tauri-plugin-log"
|
||||
branch = "dev"
|
||||
features = ["colored"]
|
||||
# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
@@ -35,10 +40,3 @@ default = [ "custom-protocol" ]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
|
||||
# fix: mac v1.2.0 can not copy/paste
|
||||
# https://github.com/tauri-apps/tauri/issues/5669
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
BIN
src-tauri/icons/tray-icon-light.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
@@ -1,90 +1,108 @@
|
||||
use crate::{conf::ChatConfJson, utils};
|
||||
use crate::{
|
||||
conf::ChatConfJson,
|
||||
utils::{self, chat_root, create_file},
|
||||
};
|
||||
use log::info;
|
||||
use std::{fs, path::PathBuf};
|
||||
use tauri::{api, command, AppHandle, Manager};
|
||||
use tauri::{api, command, AppHandle, Manager, Theme};
|
||||
|
||||
#[command]
|
||||
pub fn drag_window(app: AppHandle) {
|
||||
app.get_window("core").unwrap().start_dragging().unwrap();
|
||||
app.get_window("core").unwrap().start_dragging().unwrap();
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn fullscreen(app: AppHandle) {
|
||||
let win = app.get_window("core").unwrap();
|
||||
if win.is_fullscreen().unwrap() {
|
||||
win.set_fullscreen(false).unwrap();
|
||||
} else {
|
||||
win.set_fullscreen(true).unwrap();
|
||||
}
|
||||
let win = app.get_window("core").unwrap();
|
||||
if win.is_fullscreen().unwrap() {
|
||||
win.set_fullscreen(false).unwrap();
|
||||
} else {
|
||||
win.set_fullscreen(true).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) {
|
||||
let path = api::path::download_dir().unwrap().join(name);
|
||||
fs::write(&path, blob).unwrap();
|
||||
utils::open_file(path);
|
||||
let path = chat_root().join(PathBuf::from(name));
|
||||
create_file(&path).unwrap();
|
||||
fs::write(&path, blob).unwrap();
|
||||
utils::open_file(path);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_file(_app: AppHandle, name: String, content: String) {
|
||||
let path = chat_root().join(PathBuf::from(name));
|
||||
create_file(&path).unwrap();
|
||||
fs::write(&path, content).unwrap();
|
||||
utils::open_file(path);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn open_link(app: AppHandle, url: String) {
|
||||
api::shell::open(&app.shell_scope(), url, None).unwrap();
|
||||
api::shell::open(&app.shell_scope(), url, None).unwrap();
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn get_chat_conf() -> ChatConfJson {
|
||||
ChatConfJson::get_chat_conf()
|
||||
ChatConfJson::get_chat_conf()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn reset_chat_conf() -> ChatConfJson {
|
||||
ChatConfJson::reset_chat_conf()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn get_theme() -> String {
|
||||
ChatConfJson::theme().unwrap_or(Theme::Light).to_string()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option<bool>) {
|
||||
utils::run_check_update(app, silent, has_msg);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
|
||||
ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
|
||||
ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
|
||||
let win = app.app_handle().get_window(label).unwrap();
|
||||
tauri::api::dialog::ask(
|
||||
app.app_handle().get_window(label).as_ref(),
|
||||
title,
|
||||
msg,
|
||||
move |is_cancel| {
|
||||
if is_cancel {
|
||||
win.close().unwrap();
|
||||
}
|
||||
},
|
||||
);
|
||||
let win = app.app_handle().get_window(label).unwrap();
|
||||
tauri::api::dialog::ask(
|
||||
app.app_handle().get_window(label).as_ref(),
|
||||
title,
|
||||
msg,
|
||||
move |is_cancel| {
|
||||
if is_cancel {
|
||||
win.close().unwrap();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
|
||||
let win = app.app_handle().get_window(label);
|
||||
tauri::api::dialog::message(win.as_ref(), title, msg);
|
||||
let win = app.app_handle().get_window(label);
|
||||
tauri::api::dialog::message(win.as_ref(), title, msg);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn open_file(path: PathBuf) {
|
||||
utils::open_file(path);
|
||||
utils::open_file(path);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn get_chat_model() -> serde_json::Value {
|
||||
let path = utils::chat_root().join("chat.model.json");
|
||||
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
|
||||
serde_json::from_str(&content).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PromptRecord {
|
||||
pub act: String,
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
|
||||
let mut rdr = csv::Reader::from_reader(data.as_bytes());
|
||||
let mut list = vec![];
|
||||
for result in rdr.deserialize() {
|
||||
let record: PromptRecord = result.unwrap();
|
||||
list.push(record);
|
||||
}
|
||||
list
|
||||
pub async fn get_data(app: AppHandle, url: String, is_msg: Option<bool>) -> Option<String> {
|
||||
let is_msg = is_msg.unwrap_or(false);
|
||||
let res = if is_msg {
|
||||
utils::get_data(&url, Some(&app)).await
|
||||
} else {
|
||||
utils::get_data(&url, None).await
|
||||
};
|
||||
res.unwrap_or_else(|err| {
|
||||
info!("chatgpt_client_http_error: {}", err);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
path::PathBuf,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri::command;
|
||||
|
||||
@@ -20,101 +20,102 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Permissions {
|
||||
readonly: bool,
|
||||
#[cfg(unix)]
|
||||
mode: u32,
|
||||
readonly: bool,
|
||||
#[cfg(unix)]
|
||||
mode: u32,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UnixMetadata {
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
mode: u32,
|
||||
nlink: u64,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
rdev: u64,
|
||||
blksize: u64,
|
||||
blocks: u64,
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
mode: u32,
|
||||
nlink: u64,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
rdev: u64,
|
||||
blksize: u64,
|
||||
blocks: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
accessed_at_ms: u64,
|
||||
created_at_ms: u64,
|
||||
modified_at_ms: u64,
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
is_symlink: bool,
|
||||
size: u64,
|
||||
permissions: Permissions,
|
||||
#[cfg(unix)]
|
||||
#[serde(flatten)]
|
||||
unix: UnixMetadata,
|
||||
#[cfg(windows)]
|
||||
file_attributes: u32,
|
||||
accessed_at_ms: u64,
|
||||
pub created_at_ms: u64,
|
||||
modified_at_ms: u64,
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
is_symlink: bool,
|
||||
size: u64,
|
||||
permissions: Permissions,
|
||||
#[cfg(unix)]
|
||||
#[serde(flatten)]
|
||||
unix: UnixMetadata,
|
||||
#[cfg(windows)]
|
||||
file_attributes: u32,
|
||||
}
|
||||
|
||||
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
time.map(|t| {
|
||||
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis() as u64
|
||||
pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
time
|
||||
.map(|t| {
|
||||
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis() as u64
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn metadata(path: PathBuf) -> Result<Metadata> {
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let file_type = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
Ok(Metadata {
|
||||
accessed_at_ms: system_time_to_ms(metadata.accessed()),
|
||||
created_at_ms: system_time_to_ms(metadata.created()),
|
||||
modified_at_ms: system_time_to_ms(metadata.modified()),
|
||||
is_dir: file_type.is_dir(),
|
||||
is_file: file_type.is_file(),
|
||||
is_symlink: file_type.is_symlink(),
|
||||
size: metadata.len(),
|
||||
permissions: Permissions {
|
||||
readonly: permissions.readonly(),
|
||||
#[cfg(unix)]
|
||||
mode: permissions.mode(),
|
||||
},
|
||||
#[cfg(unix)]
|
||||
unix: UnixMetadata {
|
||||
dev: metadata.dev(),
|
||||
ino: metadata.ino(),
|
||||
mode: metadata.mode(),
|
||||
nlink: metadata.nlink(),
|
||||
uid: metadata.uid(),
|
||||
gid: metadata.gid(),
|
||||
rdev: metadata.rdev(),
|
||||
blksize: metadata.blksize(),
|
||||
blocks: metadata.blocks(),
|
||||
},
|
||||
#[cfg(windows)]
|
||||
file_attributes: metadata.file_attributes(),
|
||||
})
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let file_type = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
Ok(Metadata {
|
||||
accessed_at_ms: system_time_to_ms(metadata.accessed()),
|
||||
created_at_ms: system_time_to_ms(metadata.created()),
|
||||
modified_at_ms: system_time_to_ms(metadata.modified()),
|
||||
is_dir: file_type.is_dir(),
|
||||
is_file: file_type.is_file(),
|
||||
is_symlink: file_type.is_symlink(),
|
||||
size: metadata.len(),
|
||||
permissions: Permissions {
|
||||
readonly: permissions.readonly(),
|
||||
#[cfg(unix)]
|
||||
mode: permissions.mode(),
|
||||
},
|
||||
#[cfg(unix)]
|
||||
unix: UnixMetadata {
|
||||
dev: metadata.dev(),
|
||||
ino: metadata.ino(),
|
||||
mode: metadata.mode(),
|
||||
nlink: metadata.nlink(),
|
||||
uid: metadata.uid(),
|
||||
gid: metadata.gid(),
|
||||
rdev: metadata.rdev(),
|
||||
blksize: metadata.blksize(),
|
||||
blocks: metadata.blocks(),
|
||||
},
|
||||
#[cfg(windows)]
|
||||
file_attributes: metadata.file_attributes(),
|
||||
})
|
||||
}
|
||||
|
||||
// #[command]
|
||||
|
||||
294
src-tauri/src/app/gpt.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use crate::{
|
||||
app::{fs_extra, window},
|
||||
conf::GITHUB_PROMPTS_CSV_URL,
|
||||
utils::{self, chat_root},
|
||||
};
|
||||
use log::info;
|
||||
use regex::Regex;
|
||||
use std::{collections::HashMap, fs, path::PathBuf, vec};
|
||||
use tauri::{api, command, AppHandle, Manager};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[command]
|
||||
pub fn get_chat_model_cmd() -> serde_json::Value {
|
||||
let path = utils::chat_root().join("chat.model.cmd.json");
|
||||
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
|
||||
serde_json::from_str(&content).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PromptRecord {
|
||||
pub cmd: Option<String>,
|
||||
pub act: String,
|
||||
pub prompt: String,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
|
||||
let mut rdr = csv::Reader::from_reader(data.as_bytes());
|
||||
let mut list = vec![];
|
||||
for result in rdr.deserialize() {
|
||||
let record: PromptRecord = result.unwrap_or_else(|err| {
|
||||
info!("parse_prompt_error: {}", err);
|
||||
PromptRecord {
|
||||
cmd: None,
|
||||
act: "".to_string(),
|
||||
prompt: "".to_string(),
|
||||
}
|
||||
});
|
||||
if !record.act.is_empty() {
|
||||
list.push(record);
|
||||
}
|
||||
}
|
||||
list
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct ModelRecord {
|
||||
pub cmd: String,
|
||||
pub act: String,
|
||||
pub prompt: String,
|
||||
pub tags: Vec<String>,
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn cmd_list() -> Vec<ModelRecord> {
|
||||
let mut list = vec![];
|
||||
for entry in WalkDir::new(utils::chat_root().join("cache_model"))
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let file = fs::read_to_string(entry.path().display().to_string());
|
||||
if let Ok(v) = file {
|
||||
let data: Vec<ModelRecord> = serde_json::from_str(&v).unwrap_or_else(|_| vec![]);
|
||||
let enable_list = data.into_iter().filter(|v| v.enable);
|
||||
list.extend(enable_list)
|
||||
}
|
||||
}
|
||||
// dbg!(&list);
|
||||
list.sort_by(|a, b| a.cmd.len().cmp(&b.cmd.len()));
|
||||
list
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct FileMetadata {
|
||||
pub name: String,
|
||||
pub ext: String,
|
||||
pub created: u64,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
|
||||
info!("get_download_list: {}", pathname);
|
||||
let download_path = chat_root().join(PathBuf::from(pathname));
|
||||
let content = fs::read_to_string(&download_path).unwrap_or_else(|err| {
|
||||
info!("download_list_error: {}", err);
|
||||
fs::write(&download_path, "[]").unwrap();
|
||||
"[]".to_string()
|
||||
});
|
||||
let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| {
|
||||
info!("download_list_parse_error: {}", err);
|
||||
vec![]
|
||||
});
|
||||
|
||||
(list, download_path)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
|
||||
info!("download_list: {}", pathname);
|
||||
let data = get_download_list(pathname);
|
||||
let mut list = vec![];
|
||||
let mut idmap = HashMap::new();
|
||||
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap);
|
||||
|
||||
for entry in WalkDir::new(utils::chat_root().join(dir))
|
||||
.into_iter()
|
||||
.filter_entry(|e| !utils::is_hidden(e))
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let metadata = entry.metadata().unwrap();
|
||||
if metadata.is_file() {
|
||||
let file_path = entry.path().display().to_string();
|
||||
let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap();
|
||||
let caps = re.captures(&file_path).unwrap();
|
||||
let fid = &caps["id"];
|
||||
let fext = &caps["ext"];
|
||||
|
||||
let mut file_data = FileMetadata {
|
||||
name: fid.to_string(),
|
||||
id: fid.to_string(),
|
||||
ext: fext.to_string(),
|
||||
created: fs_extra::system_time_to_ms(metadata.created()),
|
||||
};
|
||||
|
||||
if idmap.get(fid).is_some() {
|
||||
let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
|
||||
match name {
|
||||
serde_json::Value::String(v) => {
|
||||
file_data.name = v.clone();
|
||||
v
|
||||
}
|
||||
_ => "".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if filename.is_some() && id.is_some() {
|
||||
if let Some(ref v) = id {
|
||||
if fid == v {
|
||||
if let Some(ref v2) = filename {
|
||||
file_data.name = v2.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
list.push(serde_json::to_value(file_data).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// dbg!(&list);
|
||||
list.sort_by(|a, b| {
|
||||
let a1 = a.get("created").unwrap().as_u64().unwrap();
|
||||
let b1 = b.get("created").unwrap().as_u64().unwrap();
|
||||
a1.cmp(&b1).reverse()
|
||||
});
|
||||
|
||||
fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> {
|
||||
let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(v) = res {
|
||||
let data = parse_prompt(v)
|
||||
.iter()
|
||||
.map(move |i| ModelRecord {
|
||||
cmd: if i.cmd.is_some() {
|
||||
i.cmd.clone().unwrap()
|
||||
} else {
|
||||
utils::gen_cmd(i.act.clone())
|
||||
},
|
||||
act: i.act.clone(),
|
||||
prompt: i.prompt.clone(),
|
||||
tags: vec!["chatgpt-prompts".to_string()],
|
||||
enable: true,
|
||||
})
|
||||
.collect::<Vec<ModelRecord>>();
|
||||
|
||||
let data2 = data.clone();
|
||||
|
||||
let model = utils::chat_root().join("chat.model.json");
|
||||
let model_cmd = utils::chat_root().join("chat.model.cmd.json");
|
||||
let chatgpt_prompts = utils::chat_root()
|
||||
.join("cache_model")
|
||||
.join("chatgpt_prompts.json");
|
||||
|
||||
if !utils::exists(&model) {
|
||||
fs::write(
|
||||
&model,
|
||||
serde_json::json!({
|
||||
"name": "ChatGPT Model",
|
||||
"link": "https://github.com/lencx/ChatGPT"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// chatgpt_prompts.json
|
||||
fs::write(
|
||||
chatgpt_prompts,
|
||||
serde_json::to_string_pretty(&data).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let cmd_data = cmd_list();
|
||||
|
||||
// chat.model.cmd.json
|
||||
fs::write(
|
||||
model_cmd,
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"name": "ChatGPT CMD",
|
||||
"last_updated": time,
|
||||
"data": cmd_data,
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut kv = HashMap::new();
|
||||
kv.insert(
|
||||
"sync_prompts".to_string(),
|
||||
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
|
||||
);
|
||||
let model_data = utils::merge(
|
||||
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
|
||||
&kv,
|
||||
);
|
||||
|
||||
// chat.model.json
|
||||
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
|
||||
|
||||
// refresh window
|
||||
api::dialog::message(
|
||||
app.get_window("core").as_ref(),
|
||||
"Sync Prompts",
|
||||
"ChatGPT Prompts data has been synchronized!",
|
||||
);
|
||||
window::window_reload(app.clone(), "core");
|
||||
window::window_reload(app, "tray");
|
||||
|
||||
return Some(data2);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<ModelRecord>> {
|
||||
let res = utils::get_data(&url, None).await.unwrap_or_else(|err| {
|
||||
info!("chatgpt_http_error: {}", err);
|
||||
None
|
||||
});
|
||||
|
||||
info!("chatgpt_http_url: {}", url);
|
||||
|
||||
if let Some(v) = res {
|
||||
let data;
|
||||
if data_type == "csv" {
|
||||
info!("chatgpt_http_csv_parse");
|
||||
data = parse_prompt(v);
|
||||
} else if data_type == "json" {
|
||||
info!("chatgpt_http_json_parse");
|
||||
data = serde_json::from_str(&v).unwrap_or_else(|err| {
|
||||
info!("chatgpt_http_json_parse_error: {}", err);
|
||||
vec![]
|
||||
});
|
||||
} else {
|
||||
info!("chatgpt_http_unknown_type");
|
||||
data = vec![];
|
||||
}
|
||||
|
||||
let data = data
|
||||
.iter()
|
||||
.map(move |i| ModelRecord {
|
||||
cmd: if i.cmd.is_some() {
|
||||
i.cmd.clone().unwrap()
|
||||
} else {
|
||||
utils::gen_cmd(i.act.clone())
|
||||
},
|
||||
act: i.act.clone(),
|
||||
prompt: i.prompt.clone(),
|
||||
tags: vec!["user-sync".to_string()],
|
||||
enable: true,
|
||||
})
|
||||
.collect::<Vec<ModelRecord>>();
|
||||
|
||||
return Some(data);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,312 +1,471 @@
|
||||
use crate::{
|
||||
conf::{self, ChatConfJson},
|
||||
utils,
|
||||
app::window,
|
||||
conf::{self, ChatConfJson},
|
||||
utils,
|
||||
};
|
||||
use tauri::{
|
||||
AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
|
||||
SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent,
|
||||
AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent,
|
||||
SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent,
|
||||
};
|
||||
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::AboutMetadata;
|
||||
|
||||
// --- Menu
|
||||
pub fn init() -> Menu {
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
let name = "ChatGPT";
|
||||
let app_menu = Submenu::new(
|
||||
name,
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
let name = "ChatGPT";
|
||||
let app_menu = Submenu::new(
|
||||
name,
|
||||
Menu::with_items([
|
||||
#[cfg(target_os = "macos")]
|
||||
MenuItem::About(name.into(), AboutMetadata::default()).into(),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
CustomMenuItem::new("about".to_string(), "About ChatGPT").into(),
|
||||
CustomMenuItem::new("check_update".to_string(), "Check for Updates").into(),
|
||||
MenuItem::Services.into(),
|
||||
MenuItem::Hide.into(),
|
||||
MenuItem::HideOthers.into(),
|
||||
MenuItem::ShowAll.into(),
|
||||
MenuItem::Separator.into(),
|
||||
MenuItem::Quit.into(),
|
||||
]),
|
||||
);
|
||||
|
||||
let stay_on_top =
|
||||
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
|
||||
let stay_on_top_menu = if chat_conf.stay_on_top {
|
||||
stay_on_top.selected()
|
||||
} else {
|
||||
stay_on_top
|
||||
};
|
||||
|
||||
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
|
||||
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
|
||||
let theme_system = CustomMenuItem::new("theme_system".to_string(), "System");
|
||||
let is_dark = chat_conf.theme == "Dark";
|
||||
let is_system = chat_conf.theme == "System";
|
||||
|
||||
let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt");
|
||||
let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent");
|
||||
let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable");
|
||||
|
||||
let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search");
|
||||
let popup_search_menu = if chat_conf.popup_search {
|
||||
popup_search.selected()
|
||||
} else {
|
||||
popup_search
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let titlebar = CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
||||
#[cfg(target_os = "macos")]
|
||||
let titlebar_menu = if chat_conf.titlebar {
|
||||
titlebar.selected()
|
||||
} else {
|
||||
titlebar
|
||||
};
|
||||
|
||||
let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray");
|
||||
let system_tray_menu = if chat_conf.tray {
|
||||
system_tray.selected()
|
||||
} else {
|
||||
system_tray
|
||||
};
|
||||
|
||||
let preferences_menu = Submenu::new(
|
||||
"Preferences",
|
||||
Menu::with_items([
|
||||
CustomMenuItem::new("control_center".to_string(), "Control Center")
|
||||
.accelerator("CmdOrCtrl+Shift+P")
|
||||
.into(),
|
||||
MenuItem::Separator.into(),
|
||||
stay_on_top_menu.into(),
|
||||
#[cfg(target_os = "macos")]
|
||||
titlebar_menu.into(),
|
||||
#[cfg(target_os = "macos")]
|
||||
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
||||
system_tray_menu.into(),
|
||||
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
|
||||
.accelerator("CmdOrCtrl+J")
|
||||
.into(),
|
||||
MenuItem::Separator.into(),
|
||||
Submenu::new(
|
||||
"Theme",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::About(name.into(), AboutMetadata::default()))
|
||||
.add_native_item(MenuItem::Services)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Hide)
|
||||
.add_native_item(MenuItem::HideOthers)
|
||||
.add_native_item(MenuItem::ShowAll)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Quit),
|
||||
);
|
||||
|
||||
let stay_on_top =
|
||||
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
|
||||
let titlebar =
|
||||
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
||||
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
|
||||
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
|
||||
let is_dark = chat_conf.theme == "Dark";
|
||||
|
||||
let stay_on_top_menu = if chat_conf.stay_on_top {
|
||||
stay_on_top.selected()
|
||||
} else {
|
||||
stay_on_top
|
||||
};
|
||||
let titlebar_menu = if chat_conf.titlebar {
|
||||
titlebar.selected()
|
||||
} else {
|
||||
titlebar
|
||||
};
|
||||
|
||||
let preferences_menu = Submenu::new(
|
||||
"Preferences",
|
||||
Menu::with_items([
|
||||
Submenu::new(
|
||||
"Theme",
|
||||
Menu::new()
|
||||
.add_item(if is_dark {
|
||||
theme_light
|
||||
} else {
|
||||
theme_light.selected()
|
||||
})
|
||||
.add_item(if is_dark {
|
||||
theme_dark.selected()
|
||||
} else {
|
||||
theme_dark
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
stay_on_top_menu.into(),
|
||||
#[cfg(target_os = "macos")]
|
||||
titlebar_menu.into(),
|
||||
#[cfg(target_os = "macos")]
|
||||
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
||||
MenuItem::Separator.into(),
|
||||
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
|
||||
.accelerator("CmdOrCtrl+J")
|
||||
.into(),
|
||||
CustomMenuItem::new("control_center".to_string(), "Control Center")
|
||||
.accelerator("CmdOrCtrl+Shift+P")
|
||||
.into(),
|
||||
MenuItem::Separator.into(),
|
||||
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
|
||||
.accelerator("CmdOrCtrl+Shift+G")
|
||||
.into(),
|
||||
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
|
||||
.accelerator("CmdOrCtrl+Shift+D")
|
||||
.into(),
|
||||
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
|
||||
.accelerator("CmdOrCtrl+Shift+R")
|
||||
.into(),
|
||||
MenuItem::Separator.into(),
|
||||
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
|
||||
.accelerator("CmdOrCtrl+Shift+A")
|
||||
.into(),
|
||||
]),
|
||||
);
|
||||
|
||||
let edit_menu = Submenu::new(
|
||||
"Edit",
|
||||
.add_item(if is_dark || is_system {
|
||||
theme_light
|
||||
} else {
|
||||
theme_light.selected()
|
||||
})
|
||||
.add_item(if is_dark {
|
||||
theme_dark.selected()
|
||||
} else {
|
||||
theme_dark
|
||||
})
|
||||
.add_item(if is_system {
|
||||
theme_system.selected()
|
||||
} else {
|
||||
theme_system
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
Submenu::new(
|
||||
"Auto Update",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::SelectAll),
|
||||
);
|
||||
|
||||
let view_menu = Submenu::new(
|
||||
"View",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("go_forward".to_string(), "Go Forward")
|
||||
.accelerator("CmdOrCtrl+Right"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen")
|
||||
.accelerator("CmdOrCtrl+Up"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
|
||||
.accelerator("CmdOrCtrl+Down"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("reload".to_string(), "Refresh the Screen")
|
||||
.accelerator("CmdOrCtrl+R"),
|
||||
),
|
||||
);
|
||||
|
||||
let window_menu = Submenu::new(
|
||||
"Window",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::Minimize)
|
||||
.add_native_item(MenuItem::Zoom),
|
||||
);
|
||||
|
||||
let help_menu = Submenu::new(
|
||||
"Help",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"chatgpt_log".to_string(),
|
||||
"ChatGPT Log",
|
||||
))
|
||||
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
|
||||
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+I"),
|
||||
),
|
||||
);
|
||||
.add_item(if chat_conf.auto_update == "Prompt" {
|
||||
update_prompt.selected()
|
||||
} else {
|
||||
update_prompt
|
||||
})
|
||||
.add_item(if chat_conf.auto_update == "Silent" {
|
||||
update_silent.selected()
|
||||
} else {
|
||||
update_silent
|
||||
}), // .add_item(if chat_conf.auto_update == "Disable" {
|
||||
// update_disable.selected()
|
||||
// } else {
|
||||
// update_disable
|
||||
// })
|
||||
)
|
||||
.into(),
|
||||
MenuItem::Separator.into(),
|
||||
popup_search_menu.into(),
|
||||
CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
|
||||
MenuItem::Separator.into(),
|
||||
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
|
||||
.accelerator("CmdOrCtrl+Shift+G")
|
||||
.into(),
|
||||
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
|
||||
.accelerator("CmdOrCtrl+Shift+R")
|
||||
.into(),
|
||||
CustomMenuItem::new("clear_conf".to_string(), "Clear Config").into(),
|
||||
MenuItem::Separator.into(),
|
||||
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT").into(),
|
||||
CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
|
||||
]),
|
||||
);
|
||||
|
||||
let edit_menu = Submenu::new(
|
||||
"Edit",
|
||||
Menu::new()
|
||||
.add_submenu(app_menu)
|
||||
.add_submenu(preferences_menu)
|
||||
.add_submenu(edit_menu)
|
||||
.add_submenu(view_menu)
|
||||
.add_submenu(window_menu)
|
||||
.add_submenu(help_menu)
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::SelectAll),
|
||||
);
|
||||
|
||||
let view_menu = Submenu::new(
|
||||
"View",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("go_forward".to_string(), "Go Forward").accelerator("CmdOrCtrl+Right"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen")
|
||||
.accelerator("CmdOrCtrl+Up"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
|
||||
.accelerator("CmdOrCtrl+Down"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_0".to_string(), "Zoom to Actual Size").accelerator("CmdOrCtrl+0"),
|
||||
)
|
||||
.add_item(CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"))
|
||||
.add_item(CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("reload".to_string(), "Refresh the Screen").accelerator("CmdOrCtrl+R"),
|
||||
),
|
||||
);
|
||||
|
||||
let window_menu = Submenu::new(
|
||||
"Window",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Minimize)
|
||||
.add_native_item(MenuItem::Zoom),
|
||||
);
|
||||
|
||||
let help_menu = Submenu::new(
|
||||
"Help",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"chatgpt_log".to_string(),
|
||||
"ChatGPT Log",
|
||||
))
|
||||
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
|
||||
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
|
||||
.accelerator("CmdOrCtrl+Shift+I"),
|
||||
),
|
||||
);
|
||||
|
||||
Menu::new()
|
||||
.add_submenu(app_menu)
|
||||
.add_submenu(preferences_menu)
|
||||
.add_submenu(window_menu)
|
||||
.add_submenu(edit_menu)
|
||||
.add_submenu(view_menu)
|
||||
.add_submenu(help_menu)
|
||||
}
|
||||
|
||||
// --- Menu Event
|
||||
pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
||||
let win = Some(event.window()).unwrap();
|
||||
let app = win.app_handle();
|
||||
let state: tauri::State<conf::ChatState> = app.state();
|
||||
let script_path = utils::script_path().to_string_lossy().to_string();
|
||||
let menu_id = event.menu_item_id();
|
||||
let win = Some(event.window()).unwrap();
|
||||
let app = win.app_handle();
|
||||
let script_path = utils::script_path().to_string_lossy().to_string();
|
||||
let menu_id = event.menu_item_id();
|
||||
let menu_handle = win.menu_handle();
|
||||
|
||||
let core_window = app.get_window("core").unwrap();
|
||||
let menu_handle = core_window.menu_handle();
|
||||
|
||||
match menu_id {
|
||||
// Preferences
|
||||
"control_center" => app.get_window("main").unwrap().show().unwrap(),
|
||||
"restart" => tauri::api::process::restart(&app.env()),
|
||||
"inject_script" => open(&app, script_path),
|
||||
"go_conf" => utils::open_file(utils::chat_root()),
|
||||
"clear_conf" => utils::clear_conf(&app),
|
||||
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
|
||||
"hide_dock_icon" => {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
|
||||
}
|
||||
"titlebar" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
ChatConfJson::amend(
|
||||
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tauri::api::process::restart(&app.env());
|
||||
}
|
||||
"theme_light" | "theme_dark" => {
|
||||
let theme = if menu_id == "theme_dark" {
|
||||
"Dark"
|
||||
} else {
|
||||
"Light"
|
||||
};
|
||||
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
|
||||
}
|
||||
"stay_on_top" => {
|
||||
let mut stay_on_top = state.stay_on_top.lock().unwrap();
|
||||
*stay_on_top = !*stay_on_top;
|
||||
menu_handle
|
||||
.get_item(menu_id)
|
||||
.set_selected(*stay_on_top)
|
||||
.unwrap();
|
||||
win.set_always_on_top(*stay_on_top).unwrap();
|
||||
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": *stay_on_top }), None).unwrap();
|
||||
}
|
||||
// View
|
||||
"reload" => win.eval("window.location.reload()").unwrap(),
|
||||
"go_back" => win.eval("window.history.go(-1)").unwrap(),
|
||||
"go_forward" => win.eval("window.history.go(1)").unwrap(),
|
||||
"scroll_top" => win
|
||||
.eval(
|
||||
r#"window.scroll({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth"
|
||||
})"#,
|
||||
)
|
||||
.unwrap(),
|
||||
"scroll_bottom" => win
|
||||
.eval(
|
||||
r#"window.scroll({
|
||||
top: document.body.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth"})"#,
|
||||
)
|
||||
.unwrap(),
|
||||
// Help
|
||||
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")),
|
||||
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
|
||||
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
|
||||
"dev_tools" => {
|
||||
win.open_devtools();
|
||||
win.close_devtools();
|
||||
}
|
||||
_ => (),
|
||||
match menu_id {
|
||||
// App
|
||||
"about" => {
|
||||
let tauri_conf = utils::get_tauri_conf().unwrap();
|
||||
tauri::api::dialog::message(
|
||||
app.get_window("core").as_ref(),
|
||||
"ChatGPT",
|
||||
format!("Version {}", tauri_conf.package.version.unwrap()),
|
||||
);
|
||||
}
|
||||
"check_update" => {
|
||||
utils::run_check_update(app, false, None);
|
||||
}
|
||||
// Preferences
|
||||
"control_center" => window::control_window(app),
|
||||
"restart" => tauri::api::process::restart(&app.env()),
|
||||
"inject_script" => open(&app, script_path),
|
||||
"go_conf" => utils::open_file(utils::chat_root()),
|
||||
"clear_conf" => utils::clear_conf(&app),
|
||||
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
|
||||
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
|
||||
"popup_search" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
let popup_search = !chat_conf.popup_search;
|
||||
menu_handle
|
||||
.get_item(menu_id)
|
||||
.set_selected(popup_search)
|
||||
.unwrap();
|
||||
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None).unwrap();
|
||||
window::window_reload(app.clone(), "core");
|
||||
window::window_reload(app, "tray");
|
||||
}
|
||||
"sync_prompts" => {
|
||||
tauri::api::dialog::ask(
|
||||
app.get_window("core").as_ref(),
|
||||
"Sync Prompts",
|
||||
"Data sync will enable all prompts, are you sure you want to sync?",
|
||||
move |is_restart| {
|
||||
if is_restart {
|
||||
app
|
||||
.get_window("core")
|
||||
.unwrap()
|
||||
.eval("window.__sync_prompts && window.__sync_prompts()")
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
"hide_dock_icon" => {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
|
||||
}
|
||||
"titlebar" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
ChatConfJson::amend(
|
||||
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tauri::api::process::restart(&app.env());
|
||||
}
|
||||
"system_tray" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap();
|
||||
tauri::api::process::restart(&app.env());
|
||||
}
|
||||
"theme_light" | "theme_dark" | "theme_system" => {
|
||||
let theme = match menu_id {
|
||||
"theme_dark" => "Dark",
|
||||
"theme_system" => "System",
|
||||
_ => "Light",
|
||||
};
|
||||
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
|
||||
}
|
||||
"update_prompt" | "update_silent" | "update_disable" => {
|
||||
// for id in ["update_prompt", "update_silent", "update_disable"] {
|
||||
for id in ["update_prompt", "update_silent"] {
|
||||
menu_handle.get_item(id).set_selected(false).unwrap();
|
||||
}
|
||||
let auto_update = match menu_id {
|
||||
"update_silent" => {
|
||||
menu_handle
|
||||
.get_item("update_silent")
|
||||
.set_selected(true)
|
||||
.unwrap();
|
||||
"Silent"
|
||||
}
|
||||
"update_disable" => {
|
||||
menu_handle
|
||||
.get_item("update_disable")
|
||||
.set_selected(true)
|
||||
.unwrap();
|
||||
"Disable"
|
||||
}
|
||||
_ => {
|
||||
menu_handle
|
||||
.get_item("update_prompt")
|
||||
.set_selected(true)
|
||||
.unwrap();
|
||||
"Prompt"
|
||||
}
|
||||
};
|
||||
ChatConfJson::amend(&serde_json::json!({ "auto_update": auto_update }), None).unwrap();
|
||||
}
|
||||
"stay_on_top" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
let stay_on_top = !chat_conf.stay_on_top;
|
||||
menu_handle
|
||||
.get_item(menu_id)
|
||||
.set_selected(stay_on_top)
|
||||
.unwrap();
|
||||
win.set_always_on_top(stay_on_top).unwrap();
|
||||
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": stay_on_top }), None).unwrap();
|
||||
}
|
||||
// Window
|
||||
"dalle2" => window::dalle2_window(&app, None, None, Some(false)),
|
||||
// View
|
||||
"zoom_0" => win.eval("window.__zoom0 && window.__zoom0()").unwrap(),
|
||||
"zoom_out" => win.eval("window.__zoomOut && window.__zoomOut()").unwrap(),
|
||||
"zoom_in" => win.eval("window.__zoomIn && window.__zoomIn()").unwrap(),
|
||||
"reload" => win.eval("window.location.reload()").unwrap(),
|
||||
"go_back" => win.eval("window.history.go(-1)").unwrap(),
|
||||
"go_forward" => win.eval("window.history.go(1)").unwrap(),
|
||||
"scroll_top" => win
|
||||
.eval(
|
||||
r#"window.scroll({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth"
|
||||
})"#,
|
||||
)
|
||||
.unwrap(),
|
||||
"scroll_bottom" => win
|
||||
.eval(
|
||||
r#"window.scroll({
|
||||
top: document.body.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth"})"#,
|
||||
)
|
||||
.unwrap(),
|
||||
// Help
|
||||
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")),
|
||||
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
|
||||
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
|
||||
"dev_tools" => {
|
||||
win.open_devtools();
|
||||
win.close_devtools();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// --- SystemTray Menu
|
||||
pub fn tray_menu() -> SystemTray {
|
||||
if cfg!(target_os = "macos") {
|
||||
SystemTray::new().with_menu(
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"control_center".to_string(),
|
||||
"Control Center",
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"show_dock_icon".to_string(),
|
||||
"Show Dock Icon",
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"hide_dock_icon".to_string(),
|
||||
"Hide Dock Icon",
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"control_center".to_string(),
|
||||
"Control Center",
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"show_dock_icon".to_string(),
|
||||
"Show Dock Icon",
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"hide_dock_icon".to_string(),
|
||||
"Hide Dock Icon",
|
||||
))
|
||||
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
|
||||
)
|
||||
} else {
|
||||
SystemTray::new().with_menu(
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"control_center".to_string(),
|
||||
"Control Center",
|
||||
))
|
||||
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- SystemTray Event
|
||||
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
|
||||
on_tray_event(handle, &event);
|
||||
on_tray_event(handle, &event);
|
||||
|
||||
let app = handle.clone();
|
||||
let app = handle.clone();
|
||||
|
||||
match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
|
||||
if !chat_conf.hide_dock_icon {
|
||||
let core_win = handle.get_window("core").unwrap();
|
||||
core_win.minimize().unwrap();
|
||||
}
|
||||
if !chat_conf.hide_dock_icon {
|
||||
let core_win = handle.get_window("core").unwrap();
|
||||
core_win.minimize().unwrap();
|
||||
}
|
||||
|
||||
let tray_win = handle.get_window("tray").unwrap();
|
||||
tray_win.move_window(Position::TrayCenter).unwrap();
|
||||
let tray_win = handle.get_window("tray").unwrap();
|
||||
tray_win.move_window(Position::TrayCenter).unwrap();
|
||||
|
||||
if tray_win.is_visible().unwrap() {
|
||||
tray_win.hide().unwrap();
|
||||
} else {
|
||||
tray_win.show().unwrap();
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"control_center" => app.get_window("main").unwrap().show().unwrap(),
|
||||
"restart" => tauri::api::process::restart(&handle.env()),
|
||||
"show_dock_icon" => {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app))
|
||||
.unwrap();
|
||||
}
|
||||
"hide_dock_icon" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
if !chat_conf.hide_dock_icon {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
"quit" => std::process::exit(0),
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
if tray_win.is_visible().unwrap() {
|
||||
tray_win.hide().unwrap();
|
||||
} else {
|
||||
tray_win.show().unwrap();
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"control_center" => window::control_window(app),
|
||||
"restart" => tauri::api::process::restart(&handle.env()),
|
||||
"show_dock_icon" => {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app)).unwrap();
|
||||
}
|
||||
"hide_dock_icon" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
if !chat_conf.hide_dock_icon {
|
||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap();
|
||||
}
|
||||
}
|
||||
"show_core" => {
|
||||
let core_win = app.get_window("core").unwrap();
|
||||
let tray_win = app.get_window("tray").unwrap();
|
||||
if !core_win.is_visible().unwrap() {
|
||||
core_win.show().unwrap();
|
||||
core_win.set_focus().unwrap();
|
||||
tray_win.hide().unwrap();
|
||||
}
|
||||
}
|
||||
"quit" => std::process::exit(0),
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(app: &AppHandle, path: String) {
|
||||
tauri::api::shell::open(&app.shell_scope(), path, None).unwrap();
|
||||
tauri::api::shell::open(&app.shell_scope(), path, None).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod cmd;
|
||||
pub mod fs_extra;
|
||||
pub mod gpt;
|
||||
pub mod menu;
|
||||
pub mod setup;
|
||||
pub mod window;
|
||||
|
||||
@@ -1,61 +1,102 @@
|
||||
use crate::{app::window, conf::ChatConfJson, utils};
|
||||
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, Manager};
|
||||
use log::info;
|
||||
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcutManager, Manager};
|
||||
use wry::application::accelerator::Accelerator;
|
||||
|
||||
pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
let url = chat_conf.origin.to_string();
|
||||
let theme = ChatConfJson::theme();
|
||||
let handle = app.app_handle();
|
||||
info!("stepup");
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
let url = chat_conf.main_origin.to_string();
|
||||
let theme = ChatConfJson::theme();
|
||||
let handle = app.app_handle();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
window::tray_window(&handle);
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
window::tray_window(&handle);
|
||||
});
|
||||
|
||||
if chat_conf.hide_dock_icon {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
} else {
|
||||
let app = app.handle();
|
||||
std::thread::spawn(move || {
|
||||
#[cfg(target_os = "macos")]
|
||||
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
|
||||
.title("ChatGPT")
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(800.0, 600.0)
|
||||
.hidden_title(true)
|
||||
.theme(theme)
|
||||
.always_on_top(chat_conf.stay_on_top)
|
||||
.title_bar_style(ChatConfJson::titlebar())
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/export.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.user_agent(&chat_conf.ua_window)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
|
||||
.title("ChatGPT")
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(800.0, 600.0)
|
||||
.theme(theme)
|
||||
.always_on_top(chat_conf.stay_on_top)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/export.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.user_agent(&chat_conf.ua_window)
|
||||
.build()
|
||||
.unwrap();
|
||||
});
|
||||
if let Some(v) = chat_conf.global_shortcut {
|
||||
info!("global_shortcut: `{}`", v);
|
||||
match v.parse::<Accelerator>() {
|
||||
Ok(_) => {
|
||||
info!("global_shortcut_register");
|
||||
let handle = app.app_handle();
|
||||
let mut shortcut = app.global_shortcut_manager();
|
||||
shortcut
|
||||
.register(&v, move || {
|
||||
if let Some(w) = handle.get_window("core") {
|
||||
if w.is_visible().unwrap() {
|
||||
w.hide().unwrap();
|
||||
} else {
|
||||
w.show().unwrap();
|
||||
w.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
info!("global_shortcut_register_error: {}", err);
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
info!("global_shortcut_parse_error: {}", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("global_shortcut_unregister");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
if chat_conf.hide_dock_icon {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
} else {
|
||||
let app = app.handle();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let link = if chat_conf.main_dashboard {
|
||||
"index.html"
|
||||
} else {
|
||||
&url
|
||||
};
|
||||
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(link.into()))
|
||||
.title("ChatGPT")
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(800.0, 600.0)
|
||||
.theme(theme)
|
||||
.always_on_top(chat_conf.stay_on_top)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.user_agent(&chat_conf.ua_window);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
main_win = main_win
|
||||
.title_bar_style(ChatConfJson::titlebar())
|
||||
.hidden_title(true);
|
||||
}
|
||||
|
||||
if url == "https://chat.openai.com" && !chat_conf.main_dashboard {
|
||||
main_win = main_win
|
||||
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
|
||||
.initialization_script(include_str!("../vendors/html2canvas.js"))
|
||||
.initialization_script(include_str!("../vendors/jspdf.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
|
||||
.initialization_script(include_str!("../scripts/popup.core.js"))
|
||||
.initialization_script(include_str!("../scripts/export.js"))
|
||||
.initialization_script(include_str!("../scripts/markdown.export.js"))
|
||||
.initialization_script(include_str!("../scripts/cmd.js"))
|
||||
}
|
||||
|
||||
main_win.build().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// auto_update
|
||||
if chat_conf.auto_update != "Disable" {
|
||||
info!("stepup::run_check_update");
|
||||
let app = app.handle();
|
||||
utils::run_check_update(app, chat_conf.auto_update == "Silent", None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,30 +1,167 @@
|
||||
use crate::{conf, utils};
|
||||
use tauri::{utils::config::WindowUrl, window::WindowBuilder};
|
||||
use log::info;
|
||||
use std::time::SystemTime;
|
||||
use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager};
|
||||
|
||||
pub fn tray_window(handle: &tauri::AppHandle) {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
let theme = conf::ChatConfJson::theme();
|
||||
let app = handle.clone();
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
let theme = conf::ChatConfJson::theme();
|
||||
let app = handle.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into()))
|
||||
.title("ChatGPT")
|
||||
.resizable(false)
|
||||
.fullscreen(false)
|
||||
.inner_size(360.0, 540.0)
|
||||
.decorations(false)
|
||||
.always_on_top(true)
|
||||
.theme(theme)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/export.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.user_agent(&chat_conf.ua_tray)
|
||||
.build()
|
||||
.unwrap()
|
||||
.hide()
|
||||
.unwrap();
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let link = if chat_conf.tray_dashboard {
|
||||
"index.html"
|
||||
} else {
|
||||
&chat_conf.tray_origin
|
||||
};
|
||||
let mut tray_win = WindowBuilder::new(&app, "tray", WindowUrl::App(link.into()))
|
||||
.title("ChatGPT")
|
||||
.resizable(false)
|
||||
.fullscreen(false)
|
||||
.inner_size(360.0, 540.0)
|
||||
.decorations(false)
|
||||
.always_on_top(true)
|
||||
.theme(theme)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.user_agent(&chat_conf.ua_tray);
|
||||
|
||||
if chat_conf.tray_origin == "https://chat.openai.com" && !chat_conf.tray_dashboard {
|
||||
tray_win = tray_win
|
||||
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
|
||||
.initialization_script(include_str!("../scripts/cmd.js"))
|
||||
.initialization_script(include_str!("../scripts/popup.core.js"))
|
||||
}
|
||||
|
||||
tray_win.build().unwrap().hide().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn dalle2_window(
|
||||
handle: &tauri::AppHandle,
|
||||
query: Option<String>,
|
||||
title: Option<String>,
|
||||
is_new: Option<bool>,
|
||||
) {
|
||||
info!("dalle2_query: {:?}", query);
|
||||
let theme = conf::ChatConfJson::theme();
|
||||
let app = handle.clone();
|
||||
|
||||
let query = if query.is_some() {
|
||||
format!("window.addEventListener('DOMContentLoaded', function() {{\nwindow.__CHATGPT_QUERY__='{}';\n}})", query.unwrap())
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let label = if is_new.unwrap_or(true) {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
format!("dalle2_{}", timestamp)
|
||||
} else {
|
||||
"dalle2".to_string()
|
||||
};
|
||||
|
||||
if app.get_window("dalle2").is_none() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
WindowBuilder::new(
|
||||
&app,
|
||||
label,
|
||||
WindowUrl::App("https://labs.openai.com".into()),
|
||||
)
|
||||
.title(title.unwrap_or_else(|| "DALL·E 2".to_string()))
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(800.0, 600.0)
|
||||
.always_on_top(false)
|
||||
.theme(theme)
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.initialization_script(&query)
|
||||
.initialization_script(include_str!("../scripts/dalle2.js"))
|
||||
.build()
|
||||
.unwrap();
|
||||
});
|
||||
} else {
|
||||
let dalle2_win = app.get_window("dalle2").unwrap();
|
||||
dalle2_win.show().unwrap();
|
||||
dalle2_win.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dalle2_search_window(app: tauri::AppHandle, query: String) {
|
||||
dalle2_window(
|
||||
&app.app_handle(),
|
||||
Some(query),
|
||||
Some("ChatGPT & DALL·E 2".to_string()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn control_window(handle: tauri::AppHandle) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if handle.get_window("main").is_none() {
|
||||
WindowBuilder::new(
|
||||
&handle,
|
||||
"main",
|
||||
WindowUrl::App("index.html?type=control".into()),
|
||||
)
|
||||
.title("Control Center")
|
||||
.resizable(true)
|
||||
.fullscreen(false)
|
||||
.inner_size(1200.0, 700.0)
|
||||
.min_inner_size(1000.0, 600.0)
|
||||
.build()
|
||||
.unwrap();
|
||||
} else {
|
||||
let main_win = handle.get_window("main").unwrap();
|
||||
main_win.show().unwrap();
|
||||
main_win.set_focus().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn wa_window(
|
||||
app: tauri::AppHandle,
|
||||
label: String,
|
||||
title: String,
|
||||
url: String,
|
||||
script: Option<String>,
|
||||
) {
|
||||
info!("wa_window: {} :=> {}", title, url);
|
||||
let win = app.get_window(&label);
|
||||
if win.is_none() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tauri::WindowBuilder::new(&app, label, tauri::WindowUrl::App(url.parse().unwrap()))
|
||||
.initialization_script(&script.unwrap_or_default())
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.title(title)
|
||||
.build()
|
||||
.unwrap();
|
||||
});
|
||||
} else {
|
||||
if !win.clone().unwrap().is_visible().unwrap() {
|
||||
win.clone().unwrap().show().unwrap();
|
||||
}
|
||||
win
|
||||
.clone()
|
||||
.unwrap()
|
||||
.eval("window.location.reload()")
|
||||
.unwrap();
|
||||
win.unwrap().set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn window_reload(app: tauri::AppHandle, label: &str) {
|
||||
app
|
||||
.app_handle()
|
||||
.get_window(label)
|
||||
.unwrap()
|
||||
.eval("window.location.reload()")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
161
src-tauri/src/assets/cmd.js
vendored
@@ -1,161 +0,0 @@
|
||||
// *** Core Script - CMD ***
|
||||
|
||||
function init() {
|
||||
const styleDom = document.createElement('style');
|
||||
styleDom.innerHTML = `form {
|
||||
position: relative;
|
||||
}
|
||||
.chat-model-cmd-list {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
z-index: 9999;
|
||||
}
|
||||
.chat-model-cmd-list>div {
|
||||
border: solid 2px #d8d8d8;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item {
|
||||
font-size: 12px;
|
||||
border-bottom: solid 1px #888;
|
||||
padding: 2px 4px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item b {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
color: #2a2a2a;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item i {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
color: #888;
|
||||
}`;
|
||||
document.head.append(styleDom);
|
||||
|
||||
if (window.formInterval) {
|
||||
clearInterval(window.formInterval);
|
||||
}
|
||||
window.formInterval = setInterval(() => {
|
||||
const form = document.querySelector("form");
|
||||
if (!form) return;
|
||||
clearInterval(window.formInterval);
|
||||
cmdTip();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async function cmdTip() {
|
||||
const chatModelJson = await invoke('get_chat_model') || {};
|
||||
const user_custom = chatModelJson.user_custom || [];
|
||||
const sys_sync_prompts = chatModelJson.sys_sync_prompts || [];
|
||||
const data = [...user_custom, ...sys_sync_prompts];
|
||||
if (data.length <= 0) return;
|
||||
|
||||
const modelDom = document.createElement('div');
|
||||
modelDom.classList.add('chat-model-cmd-list');
|
||||
|
||||
// fix: tray window
|
||||
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||
modelDom.style.bottom = '40px';
|
||||
}
|
||||
|
||||
document.querySelector('form').appendChild(modelDom);
|
||||
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
|
||||
const searchInput = document.querySelector('form textarea');
|
||||
|
||||
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
|
||||
// If more than one command appears in the search results, the first one will be used by default.
|
||||
searchInput.addEventListener('keydown', (event) => {
|
||||
if (!window.__CHAT_MODEL_CMD__) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.keyCode === 32) {
|
||||
searchInput.value = window.__CHAT_MODEL_CMD__;
|
||||
modelDom.innerHTML = '';
|
||||
delete window.__CHAT_MODEL_CMD__;
|
||||
}
|
||||
if (event.keyCode === 13) {
|
||||
modelDom.innerHTML = '';
|
||||
delete window.__CHAT_MODEL_CMD__;
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', (event) => {
|
||||
const query = searchInput.value;
|
||||
if (!query || !/^\//.test(query)) {
|
||||
modelDom.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// all cmd result
|
||||
if (query === '/') {
|
||||
const result = data.filter(i => i.enable);
|
||||
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
||||
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = data.filter(i => i.enable && new RegExp(query.substring(1)).test(i.cmd));
|
||||
if (result.length > 0) {
|
||||
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
||||
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
|
||||
} else {
|
||||
modelDom.innerHTML = '';
|
||||
delete window.__CHAT_MODEL_CMD__;
|
||||
}
|
||||
}, {
|
||||
capture: false,
|
||||
passive: true,
|
||||
once: false
|
||||
});
|
||||
|
||||
if (window.searchInterval) {
|
||||
clearInterval(window.searchInterval);
|
||||
}
|
||||
window.searchInterval = setInterval(() => {
|
||||
// The `chatgpt prompt` fill can be done by clicking on the event.
|
||||
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
|
||||
if (!searchDom) return;
|
||||
searchDom.addEventListener('click', (event) => {
|
||||
// .cmd-item
|
||||
const item = event.target.closest("div");
|
||||
if (item) {
|
||||
const val = decodeURIComponent(item.getAttribute('data-prompt'));
|
||||
searchInput.value = val;
|
||||
document.querySelector('form textarea').focus();
|
||||
window.__CHAT_MODEL_CMD__ = val;
|
||||
modelDom.innerHTML = '';
|
||||
}
|
||||
}, {
|
||||
capture: false,
|
||||
passive: true,
|
||||
once: false
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
98
src-tauri/src/assets/core.js
vendored
@@ -1,98 +0,0 @@
|
||||
// *** Core Script - IPC ***
|
||||
|
||||
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
function transformCallback(callback = () => {}, once = false) {
|
||||
const identifier = uid();
|
||||
const prop = `_${identifier}`;
|
||||
Object.defineProperty(window, prop, {
|
||||
value: (result) => {
|
||||
if (once) {
|
||||
Reflect.deleteProperty(window, prop);
|
||||
}
|
||||
return callback(result)
|
||||
},
|
||||
writable: false,
|
||||
configurable: true,
|
||||
})
|
||||
return identifier;
|
||||
}
|
||||
async function invoke(cmd, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
|
||||
const callback = transformCallback((e) => {
|
||||
resolve(e);
|
||||
Reflect.deleteProperty(window, `_${error}`);
|
||||
}, true)
|
||||
const error = transformCallback((e) => {
|
||||
reject(e);
|
||||
Reflect.deleteProperty(window, `_${callback}`);
|
||||
}, true)
|
||||
window.__TAURI_POST_MESSAGE__({
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
...args
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.uid = uid;
|
||||
window.invoke = invoke;
|
||||
window.transformCallback = transformCallback;
|
||||
|
||||
async function init() {
|
||||
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
|
||||
}
|
||||
|
||||
if (__TAURI_METADATA__.__currentWindow.label !== 'core') return;
|
||||
|
||||
async function platform() {
|
||||
return invoke('platform', {
|
||||
__tauriModule: 'Os',
|
||||
message: { cmd: 'platform' }
|
||||
});
|
||||
}
|
||||
|
||||
const _platform = await platform();
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (/darwin/.test(_platform) && !chatConf.titlebar) {
|
||||
const topStyleDom = document.createElement("style");
|
||||
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
|
||||
document.head.appendChild(topStyleDom);
|
||||
const topDom = document.createElement("div");
|
||||
topDom.id = "chatgpt-app-window-top";
|
||||
document.body.appendChild(topDom);
|
||||
|
||||
topDom.addEventListener("mousedown", () => invoke("drag_window"));
|
||||
topDom.addEventListener("touchstart", () => invoke("drag_window"));
|
||||
topDom.addEventListener("dblclick", () => invoke("fullscreen"));
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const origin = e.target.closest("a");
|
||||
if (origin && origin.href && origin.target !== '_self') {
|
||||
invoke('open_link', { url: origin.href });
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('wheel', function(event) {
|
||||
const deltaX = event.wheelDeltaX;
|
||||
if (Math.abs(deltaX) >= 50) {
|
||||
if (deltaX > 0) {
|
||||
window.history.go(-1);
|
||||
} else {
|
||||
window.history.go(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
233
src-tauri/src/assets/export.js
vendored
@@ -1,233 +0,0 @@
|
||||
// *** Core Script - Export ***
|
||||
// @ref: https://github.com/liady/ChatGPT-pdf
|
||||
|
||||
async function init() {
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (window.buttonsInterval) {
|
||||
clearInterval(window.buttonsInterval);
|
||||
}
|
||||
window.buttonsInterval = setInterval(() => {
|
||||
const actionsArea = document.querySelector("form>div>div");
|
||||
if (!actionsArea) {
|
||||
return;
|
||||
}
|
||||
const buttons = actionsArea.querySelectorAll("button");
|
||||
const hasTryAgainButton = Array.from(buttons).some((button) => {
|
||||
return !button.id?.includes("download");
|
||||
});
|
||||
if (hasTryAgainButton && buttons.length === 1) {
|
||||
const TryAgainButton = actionsArea.querySelector("button");
|
||||
addActionsButtons(actionsArea, TryAgainButton, chatConf);
|
||||
} else if (!hasTryAgainButton) {
|
||||
removeButtons();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const Format = {
|
||||
PNG: "png",
|
||||
PDF: "pdf",
|
||||
};
|
||||
|
||||
function addActionsButtons(actionsArea, TryAgainButton, chatConf) {
|
||||
const downloadButton = TryAgainButton.cloneNode(true);
|
||||
downloadButton.id = "download-png-button";
|
||||
downloadButton.innerText = "Generate PNG";
|
||||
downloadButton.onclick = () => {
|
||||
downloadThread();
|
||||
};
|
||||
actionsArea.appendChild(downloadButton);
|
||||
|
||||
const downloadPdfButton = TryAgainButton.cloneNode(true);
|
||||
downloadPdfButton.id = "download-pdf-button";
|
||||
downloadPdfButton.innerText = "Download PDF";
|
||||
downloadPdfButton.onclick = () => {
|
||||
downloadThread({ as: Format.PDF });
|
||||
};
|
||||
actionsArea.appendChild(downloadPdfButton);
|
||||
|
||||
if (new RegExp('//chat.openai.com').test(chatConf.origin)) {
|
||||
const exportHtml = TryAgainButton.cloneNode(true);
|
||||
exportHtml.id = "download-html-button";
|
||||
exportHtml.innerText = "Share Link";
|
||||
exportHtml.onclick = () => {
|
||||
sendRequest();
|
||||
};
|
||||
actionsArea.appendChild(exportHtml);
|
||||
}
|
||||
}
|
||||
|
||||
function removeButtons() {
|
||||
const downloadButton = document.getElementById("download-png-button");
|
||||
const downloadPdfButton = document.getElementById("download-pdf-button");
|
||||
const downloadHtmlButton = document.getElementById("download-html-button");
|
||||
if (downloadButton) {
|
||||
downloadButton.remove();
|
||||
}
|
||||
if (downloadPdfButton) {
|
||||
downloadPdfButton.remove();
|
||||
}
|
||||
if (downloadHtmlButton) {
|
||||
downloadHtmlButton.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadThread({ as = Format.PNG } = {}) {
|
||||
const elements = new Elements();
|
||||
elements.fixLocation();
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const minRatio = as === Format.PDF ? 2 : 2.5;
|
||||
window.devicePixelRatio = Math.max(pixelRatio, minRatio);
|
||||
|
||||
html2canvas(elements.thread, {
|
||||
letterRendering: true,
|
||||
}).then(async function (canvas) {
|
||||
elements.restoreLocation();
|
||||
window.devicePixelRatio = pixelRatio;
|
||||
const imgData = canvas.toDataURL("image/png");
|
||||
requestAnimationFrame(() => {
|
||||
if (as === Format.PDF) {
|
||||
return handlePdf(imgData, canvas, pixelRatio);
|
||||
} else {
|
||||
handleImg(imgData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleImg(imgData) {
|
||||
const binaryData = atob(imgData.split("base64,")[1]);
|
||||
const data = [];
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
data.push(binaryData.charCodeAt(i));
|
||||
}
|
||||
invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: Array.from(new Uint8Array(data)) });
|
||||
}
|
||||
|
||||
function handlePdf(imgData, canvas, pixelRatio) {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const orientation = canvas.width > canvas.height ? "l" : "p";
|
||||
var pdf = new jsPDF(orientation, "pt", [
|
||||
canvas.width / pixelRatio,
|
||||
canvas.height / pixelRatio,
|
||||
]);
|
||||
var pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
var pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight);
|
||||
|
||||
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
|
||||
invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) });
|
||||
}
|
||||
|
||||
class Elements {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
// this.threadWrapper = document.querySelector(".cdfdFe");
|
||||
this.spacer = document.querySelector(".w-full.h-48.flex-shrink-0");
|
||||
this.thread = document.querySelector(
|
||||
"[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div"
|
||||
);
|
||||
this.positionForm = document.querySelector("form").parentNode;
|
||||
// this.styledThread = document.querySelector("main");
|
||||
// this.threadContent = document.querySelector(".gAnhyd");
|
||||
this.scroller = Array.from(
|
||||
document.querySelectorAll('[class*="react-scroll-to"]')
|
||||
).filter((el) => el.classList.contains("h-full"))[0];
|
||||
this.hiddens = Array.from(document.querySelectorAll(".overflow-hidden"));
|
||||
this.images = Array.from(document.querySelectorAll("img[srcset]"));
|
||||
}
|
||||
fixLocation() {
|
||||
this.hiddens.forEach((el) => {
|
||||
el.classList.remove("overflow-hidden");
|
||||
});
|
||||
this.spacer.style.display = "none";
|
||||
this.thread.style.maxWidth = "960px";
|
||||
this.thread.style.marginInline = "auto";
|
||||
this.positionForm.style.display = "none";
|
||||
this.scroller.classList.remove("h-full");
|
||||
this.scroller.style.minHeight = "100vh";
|
||||
this.images.forEach((img) => {
|
||||
const srcset = img.getAttribute("srcset");
|
||||
img.setAttribute("srcset_old", srcset);
|
||||
img.setAttribute("srcset", "");
|
||||
});
|
||||
//Fix to the text shifting down when generating the canvas
|
||||
document.body.style.lineHeight = "0.5";
|
||||
}
|
||||
restoreLocation() {
|
||||
this.hiddens.forEach((el) => {
|
||||
el.classList.add("overflow-hidden");
|
||||
});
|
||||
this.spacer.style.display = null;
|
||||
this.thread.style.maxWidth = null;
|
||||
this.thread.style.marginInline = null;
|
||||
this.positionForm.style.display = null;
|
||||
this.scroller.classList.add("h-full");
|
||||
this.scroller.style.minHeight = null;
|
||||
this.images.forEach((img) => {
|
||||
const srcset = img.getAttribute("srcset_old");
|
||||
img.setAttribute("srcset", srcset);
|
||||
img.setAttribute("srcset_old", "");
|
||||
});
|
||||
document.body.style.lineHeight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function selectElementByClassPrefix(classPrefix) {
|
||||
const element = document.querySelector(`[class^='${classPrefix}']`);
|
||||
return element;
|
||||
}
|
||||
|
||||
async function sendRequest() {
|
||||
const data = getData();
|
||||
const uploadUrlResponse = await fetch(
|
||||
"https://chatgpt-static.s3.amazonaws.com/url.txt"
|
||||
);
|
||||
const uploadUrl = await uploadUrlResponse.text();
|
||||
fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
invoke('open_link', { url: data.url });
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
const globalCss = getCssFromSheet(
|
||||
document.querySelector("link[rel=stylesheet]").sheet
|
||||
);
|
||||
const localCss =
|
||||
getCssFromSheet(
|
||||
document.querySelector(`style[data-styled][data-styled-version]`).sheet
|
||||
) || "body{}";
|
||||
const data = {
|
||||
main: document.querySelector("main").outerHTML,
|
||||
// css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`,
|
||||
globalCss,
|
||||
localCss,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
function getCssFromSheet(sheet) {
|
||||
return Array.from(sheet.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// run init
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
@@ -1,175 +1,200 @@
|
||||
use crate::utils::{chat_root, create_file, exists};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex};
|
||||
use std::{collections::BTreeMap, fs, path::PathBuf};
|
||||
use tauri::{Manager, Theme};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
|
||||
// pub const USER_AGENT: &str = "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
|
||||
// pub const PHONE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
|
||||
// pub const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15";
|
||||
// pub const PHONE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1";
|
||||
|
||||
pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
|
||||
pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md";
|
||||
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
|
||||
pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx";
|
||||
pub const GITHUB_PROMPTS_CSV_URL: &str =
|
||||
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
|
||||
pub const DEFAULT_CHAT_CONF: &str = r#"{
|
||||
"stay_on_top": false,
|
||||
"theme": "Light",
|
||||
"titlebar": true,
|
||||
"hide_dock_icon": false,
|
||||
"default_origin": "https://chat.openai.com",
|
||||
"origin": "https://chat.openai.com",
|
||||
"ua_window": "",
|
||||
"ua_tray": ""
|
||||
"stay_on_top": false,
|
||||
"auto_update": "Prompt",
|
||||
"theme": "Light",
|
||||
"tray": true,
|
||||
"titlebar": true,
|
||||
"popup_search": false,
|
||||
"global_shortcut": "",
|
||||
"hide_dock_icon": false,
|
||||
"main_dashboard": false,
|
||||
"tray_dashboard": false,
|
||||
"main_origin": "https://chat.openai.com",
|
||||
"tray_origin": "https://chat.openai.com",
|
||||
"default_origin": "https://chat.openai.com",
|
||||
"ua_window": "",
|
||||
"ua_tray": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
|
||||
}"#;
|
||||
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
|
||||
"stay_on_top": false,
|
||||
"theme": "Light",
|
||||
"titlebar": false,
|
||||
"hide_dock_icon": false,
|
||||
"default_origin": "https://chat.openai.com",
|
||||
"origin": "https://chat.openai.com",
|
||||
"ua_window": "",
|
||||
"ua_tray": ""
|
||||
"stay_on_top": false,
|
||||
"auto_update": "Prompt",
|
||||
"theme": "Light",
|
||||
"tray": true,
|
||||
"titlebar": false,
|
||||
"popup_search": false,
|
||||
"global_shortcut": "",
|
||||
"hide_dock_icon": false,
|
||||
"main_dashboard": false,
|
||||
"tray_dashboard": false,
|
||||
"main_origin": "https://chat.openai.com",
|
||||
"tray_origin": "https://chat.openai.com",
|
||||
"default_origin": "https://chat.openai.com",
|
||||
"ua_window": "",
|
||||
"ua_tray": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
|
||||
}"#;
|
||||
|
||||
pub struct ChatState {
|
||||
pub stay_on_top: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl ChatState {
|
||||
pub fn default(chat_conf: ChatConfJson) -> Self {
|
||||
ChatState {
|
||||
stay_on_top: Mutex::new(chat_conf.stay_on_top),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct ChatConfJson {
|
||||
// support macOS only
|
||||
pub titlebar: bool,
|
||||
pub hide_dock_icon: bool,
|
||||
// support macOS only
|
||||
pub titlebar: bool,
|
||||
pub hide_dock_icon: bool,
|
||||
|
||||
// macOS and Windows
|
||||
pub theme: String,
|
||||
|
||||
pub stay_on_top: bool,
|
||||
pub default_origin: String,
|
||||
pub origin: String,
|
||||
pub ua_window: String,
|
||||
pub ua_tray: String,
|
||||
// macOS and Windows, Light/Dark/System
|
||||
pub theme: String,
|
||||
// auto update policy, Prompt/Silent/Disable
|
||||
pub auto_update: String,
|
||||
pub tray: bool,
|
||||
pub popup_search: bool,
|
||||
pub stay_on_top: bool,
|
||||
pub main_dashboard: bool,
|
||||
pub tray_dashboard: bool,
|
||||
pub main_origin: String,
|
||||
pub tray_origin: String,
|
||||
pub default_origin: String,
|
||||
pub ua_window: String,
|
||||
pub ua_tray: String,
|
||||
pub global_shortcut: Option<String>,
|
||||
}
|
||||
|
||||
impl ChatConfJson {
|
||||
/// init chat.conf.json
|
||||
/// path: ~/.chatgpt/chat.conf.json
|
||||
pub fn init() -> PathBuf {
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let content = if cfg!(target_os = "macos") {
|
||||
DEFAULT_CHAT_CONF_MAC
|
||||
} else {
|
||||
DEFAULT_CHAT_CONF
|
||||
};
|
||||
/// init chat.conf.json
|
||||
/// path: ~/.chatgpt/chat.conf.json
|
||||
pub fn init() -> PathBuf {
|
||||
info!("chat_conf_init");
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let content = if cfg!(target_os = "macos") {
|
||||
DEFAULT_CHAT_CONF_MAC
|
||||
} else {
|
||||
DEFAULT_CHAT_CONF
|
||||
};
|
||||
|
||||
if !exists(&conf_file) {
|
||||
create_file(&conf_file).unwrap();
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
return conf_file;
|
||||
}
|
||||
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let file_content = fs::read_to_string(&conf_file).unwrap();
|
||||
match serde_json::from_str(&file_content) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" {
|
||||
return conf_file;
|
||||
}
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
conf_file
|
||||
if !exists(&conf_file) {
|
||||
create_file(&conf_file).unwrap();
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
return conf_file;
|
||||
}
|
||||
|
||||
pub fn conf_path() -> PathBuf {
|
||||
chat_root().join("chat.conf.json")
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let file_content = fs::read_to_string(&conf_file).unwrap();
|
||||
match serde_json::from_str(&file_content) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" {
|
||||
return conf_file;
|
||||
}
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
conf_file
|
||||
}
|
||||
|
||||
pub fn conf_path() -> PathBuf {
|
||||
chat_root().join("chat.conf.json")
|
||||
}
|
||||
|
||||
pub fn get_chat_conf() -> Self {
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let file_content = fs::read_to_string(&conf_file).unwrap();
|
||||
let content = if cfg!(target_os = "macos") {
|
||||
DEFAULT_CHAT_CONF_MAC
|
||||
} else {
|
||||
DEFAULT_CHAT_CONF
|
||||
};
|
||||
|
||||
match serde_json::from_value(match serde_json::from_str(&file_content) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
serde_json::from_str(content).unwrap()
|
||||
}
|
||||
}) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_chat_conf() -> Self {
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let content = if cfg!(target_os = "macos") {
|
||||
DEFAULT_CHAT_CONF_MAC
|
||||
} else {
|
||||
DEFAULT_CHAT_CONF
|
||||
};
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
serde_json::from_str(content).unwrap()
|
||||
}
|
||||
|
||||
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
|
||||
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
|
||||
let config = ChatConfJson::get_chat_conf();
|
||||
let config: Value = serde_json::to_value(&config)?;
|
||||
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
|
||||
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
|
||||
|
||||
for (k, v) in new_rules {
|
||||
config.insert(k, v);
|
||||
}
|
||||
|
||||
pub fn get_chat_conf() -> Self {
|
||||
let conf_file = ChatConfJson::conf_path();
|
||||
let file_content = fs::read_to_string(&conf_file).unwrap();
|
||||
let content = if cfg!(target_os = "macos") {
|
||||
DEFAULT_CHAT_CONF_MAC
|
||||
} else {
|
||||
DEFAULT_CHAT_CONF
|
||||
};
|
||||
fs::write(
|
||||
ChatConfJson::conf_path(),
|
||||
serde_json::to_string_pretty(&config)?,
|
||||
)?;
|
||||
|
||||
match serde_json::from_value(match serde_json::from_str(&file_content) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
serde_json::from_str(content).unwrap()
|
||||
}
|
||||
}) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
fs::write(&conf_file, content).unwrap();
|
||||
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
if let Some(handle) = app {
|
||||
tauri::api::process::restart(&handle.env());
|
||||
}
|
||||
|
||||
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
|
||||
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
|
||||
let config = ChatConfJson::get_chat_conf();
|
||||
let config: Value = serde_json::to_value(&config)?;
|
||||
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
|
||||
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
for (k, v) in new_rules {
|
||||
config.insert(k, v);
|
||||
}
|
||||
pub fn theme() -> Option<Theme> {
|
||||
let conf = ChatConfJson::get_chat_conf();
|
||||
let theme = match conf.theme.as_str() {
|
||||
"System" => match dark_light::detect() {
|
||||
// Dark mode
|
||||
dark_light::Mode::Dark => Theme::Dark,
|
||||
// Light mode
|
||||
dark_light::Mode::Light => Theme::Light,
|
||||
// Unspecified
|
||||
dark_light::Mode::Default => Theme::Light,
|
||||
},
|
||||
"Dark" => Theme::Dark,
|
||||
_ => Theme::Light,
|
||||
};
|
||||
|
||||
fs::write(
|
||||
ChatConfJson::conf_path(),
|
||||
serde_json::to_string_pretty(&config)?,
|
||||
)?;
|
||||
Some(theme)
|
||||
}
|
||||
|
||||
if let Some(handle) = app {
|
||||
tauri::api::process::restart(&handle.env());
|
||||
// tauri::api::dialog::ask(
|
||||
// handle.get_window("core").as_ref(),
|
||||
// "ChatGPT Restart",
|
||||
// "Whether to restart immediately?",
|
||||
// move |is_restart| {
|
||||
// if is_restart {
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn theme() -> Option<Theme> {
|
||||
let conf = ChatConfJson::get_chat_conf();
|
||||
if conf.theme == "Dark" {
|
||||
Some(Theme::Dark)
|
||||
} else {
|
||||
Some(Theme::Light)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn titlebar() -> TitleBarStyle {
|
||||
let conf = ChatConfJson::get_chat_conf();
|
||||
if conf.titlebar {
|
||||
TitleBarStyle::Transparent
|
||||
} else {
|
||||
TitleBarStyle::Overlay
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn titlebar() -> TitleBarStyle {
|
||||
let conf = ChatConfJson::get_chat_conf();
|
||||
if conf.titlebar {
|
||||
TitleBarStyle::Transparent
|
||||
} else {
|
||||
TitleBarStyle::Overlay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,118 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod conf;
|
||||
mod utils;
|
||||
|
||||
use app::{cmd, fs_extra, menu, setup};
|
||||
use conf::{ChatConfJson, ChatState};
|
||||
use app::{cmd, fs_extra, gpt, menu, setup, window};
|
||||
use conf::ChatConfJson;
|
||||
use tauri::api::path;
|
||||
use tauri_plugin_log::{fern::colors::ColoredLevelConfig, LogTarget, LoggerBuilder};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_log::{
|
||||
fern::colors::{Color, ColoredLevelConfig},
|
||||
LogTarget,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
ChatConfJson::init();
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
let context = tauri::generate_context!();
|
||||
let colors = ColoredLevelConfig::default();
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
ChatConfJson::init();
|
||||
// If the file does not exist, creating the file will block menu synchronization
|
||||
utils::create_chatgpt_prompts();
|
||||
let context = tauri::generate_context!();
|
||||
let colors = ColoredLevelConfig {
|
||||
error: Color::Red,
|
||||
warn: Color::Yellow,
|
||||
debug: Color::Blue,
|
||||
info: Color::BrightGreen,
|
||||
trace: Color::Cyan,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
// https://github.com/tauri-apps/tauri/pull/2736
|
||||
.plugin(
|
||||
LoggerBuilder::new()
|
||||
// .level(log::LevelFilter::Error)
|
||||
.with_colors(colors)
|
||||
.targets([
|
||||
// LogTarget::LogDir,
|
||||
// LOG PATH: ~/.chatgpt/ChatGPT.log
|
||||
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
|
||||
LogTarget::Stdout,
|
||||
LogTarget::Webview,
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
.manage(ChatState::default(chat_conf))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::drag_window,
|
||||
cmd::fullscreen,
|
||||
cmd::download,
|
||||
cmd::open_link,
|
||||
cmd::get_chat_conf,
|
||||
cmd::form_cancel,
|
||||
cmd::form_confirm,
|
||||
cmd::form_msg,
|
||||
cmd::open_file,
|
||||
cmd::get_chat_model,
|
||||
cmd::parse_prompt,
|
||||
fs_extra::metadata,
|
||||
gpt::download_list("chat.download.json", "download", None, None);
|
||||
gpt::download_list("chat.notes.json", "notes", None, None);
|
||||
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
|
||||
let mut builder = tauri::Builder::default()
|
||||
// https://github.com/tauri-apps/tauri/pull/2736
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.targets([
|
||||
// LogTarget::LogDir,
|
||||
// LOG PATH: ~/.chatgpt/ChatGPT.log
|
||||
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
|
||||
LogTarget::Stdout,
|
||||
LogTarget::Webview,
|
||||
])
|
||||
.setup(setup::init)
|
||||
.plugin(tauri_plugin_positioner::init())
|
||||
.menu(menu::init())
|
||||
.system_tray(menu::tray_menu())
|
||||
.on_menu_event(menu::menu_handler)
|
||||
.on_system_tray_event(menu::tray_handler)
|
||||
.on_window_event(|event| {
|
||||
// https://github.com/tauri-apps/tauri/discussions/2684
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||
let win = event.window();
|
||||
if win.label() == "main" {
|
||||
win.hide().unwrap();
|
||||
} else {
|
||||
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
||||
// event.window().hide().unwrap();
|
||||
// https://github.com/tauri-apps/tao/pull/517
|
||||
event.window().minimize().unwrap();
|
||||
}
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.run(context)
|
||||
.expect("error while running ChatGPT application");
|
||||
.level(log::LevelFilter::Debug)
|
||||
.with_colors(colors)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_positioner::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::drag_window,
|
||||
cmd::fullscreen,
|
||||
cmd::download,
|
||||
cmd::save_file,
|
||||
cmd::open_link,
|
||||
cmd::get_chat_conf,
|
||||
cmd::get_theme,
|
||||
cmd::reset_chat_conf,
|
||||
cmd::run_check_update,
|
||||
cmd::form_cancel,
|
||||
cmd::form_confirm,
|
||||
cmd::form_msg,
|
||||
cmd::open_file,
|
||||
cmd::get_data,
|
||||
gpt::get_chat_model_cmd,
|
||||
gpt::parse_prompt,
|
||||
gpt::sync_prompts,
|
||||
gpt::sync_user_prompts,
|
||||
gpt::cmd_list,
|
||||
gpt::download_list,
|
||||
gpt::get_download_list,
|
||||
window::wa_window,
|
||||
window::control_window,
|
||||
window::window_reload,
|
||||
window::dalle2_search_window,
|
||||
fs_extra::metadata,
|
||||
])
|
||||
.setup(setup::init)
|
||||
.menu(menu::init());
|
||||
|
||||
if chat_conf.tray {
|
||||
builder = builder.system_tray(menu::tray_menu());
|
||||
}
|
||||
|
||||
builder
|
||||
.on_menu_event(menu::menu_handler)
|
||||
.on_system_tray_event(menu::tray_handler)
|
||||
.on_window_event(|event| {
|
||||
// https://github.com/tauri-apps/tauri/discussions/2684
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||
let win = event.window();
|
||||
if win.label() == "core" {
|
||||
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
||||
// event.window().hide().unwrap();
|
||||
// https://github.com/tauri-apps/tao/pull/517
|
||||
#[cfg(target_os = "macos")]
|
||||
event.window().minimize().unwrap();
|
||||
|
||||
// fix: https://github.com/lencx/ChatGPT/issues/93
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
event.window().hide().unwrap();
|
||||
} else {
|
||||
win.close().unwrap();
|
||||
}
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.run(context)
|
||||
.expect("error while running ChatGPT application");
|
||||
}
|
||||
|
||||
296
src-tauri/src/scripts/cmd.js
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
// *** Core Script - CMD ***
|
||||
|
||||
function init() {
|
||||
const styleDom = document.createElement('style');
|
||||
styleDom.innerHTML = `form {
|
||||
position: relative;
|
||||
}
|
||||
.chat-model-cmd-list {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
z-index: 9999;
|
||||
}
|
||||
.chat-model-cmd-list>div {
|
||||
border: solid 2px rgba(80,80,80,.3);
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
html.dark .chat-model-cmd-list>div {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
html.dark .chat-model-cmd-list .cmd-item {
|
||||
border-color: #666;
|
||||
}
|
||||
html.dark .chat-model-cmd-list .cmd-item b {
|
||||
color: #e8e8e8;
|
||||
}
|
||||
html.dark .chat-model-cmd-list .cmd-item i {
|
||||
color: #999;
|
||||
}
|
||||
html.dark .chat-model-cmd-list .cmd-item.selected {
|
||||
background: rgba(59,130,246,.5);
|
||||
}
|
||||
|
||||
.chat-model-cmd-list .cmd-item {
|
||||
font-size: 12px;
|
||||
border-bottom: solid 1px rgba(80,80,80,.2);
|
||||
padding: 2px 4px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item.selected {
|
||||
background: rgba(59,130,246,.3);
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item b {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
color: #2a2a2a;
|
||||
}
|
||||
.chat-model-cmd-list .cmd-item i {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
color: #888;
|
||||
}
|
||||
.chatappico {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.chatappico.pdf, .chatappico.md {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
#download-png-button, #download-pdf-button, #download-html-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.append(styleDom);
|
||||
|
||||
if (window.formInterval) {
|
||||
clearInterval(window.formInterval);
|
||||
}
|
||||
window.formInterval = setInterval(() => {
|
||||
const form = document.querySelector("form textarea");
|
||||
if (!form) return;
|
||||
clearInterval(window.formInterval);
|
||||
cmdTip();
|
||||
new MutationObserver(function (mutationsList) {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.target.getAttribute('id') === '__next') {
|
||||
initDom();
|
||||
cmdTip();
|
||||
}
|
||||
if (mutation.target.getAttribute('class') === 'chat-model-cmd-list') {
|
||||
// The `chatgpt prompt` fill can be done by clicking on the event.
|
||||
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
|
||||
const searchInput = document.querySelector('form textarea');
|
||||
if (!searchDom) return;
|
||||
searchDom.addEventListener('click', (event) => {
|
||||
const item = event.target.closest("div");
|
||||
if (item) {
|
||||
const val = decodeURIComponent(item.getAttribute('data-prompt'));
|
||||
searchInput.value = val;
|
||||
document.querySelector('form textarea').focus();
|
||||
initDom();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function cmdTip() {
|
||||
initDom();
|
||||
const chatModelJson = await invoke('get_chat_model_cmd') || {};
|
||||
const data = chatModelJson.data;
|
||||
if (data.length <= 0) return;
|
||||
|
||||
let modelDom = document.querySelector('.chat-model-cmd-list');
|
||||
if (!modelDom) {
|
||||
const dom = document.createElement('div');
|
||||
dom.classList.add('chat-model-cmd-list');
|
||||
document.querySelector('form').appendChild(dom);
|
||||
modelDom = document.querySelector('.chat-model-cmd-list');
|
||||
|
||||
// fix: tray window
|
||||
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||
modelDom.style.bottom = '54px';
|
||||
}
|
||||
|
||||
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-cmd="${v.cmd}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
|
||||
const renderList = (v) => {
|
||||
initDom();
|
||||
modelDom.innerHTML = `<div>${v.map(itemDom).join('')}</div>`;
|
||||
window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim();
|
||||
window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim();
|
||||
window.__cmd_list = modelDom.querySelectorAll('.cmd-item');
|
||||
window.__cmd_index = 0;
|
||||
window.__cmd_list[window.__cmd_index].classList.add('selected');
|
||||
};
|
||||
const setPrompt = (v = '') => {
|
||||
if (v.trim()) {
|
||||
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`);
|
||||
}
|
||||
}
|
||||
const searchInput = document.querySelector('form textarea');
|
||||
|
||||
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
|
||||
// If more than one command appears in the search results, the first one will be used by default.
|
||||
function cmdKeydown(event) {
|
||||
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------ Keyboard scrolling (ArrowUp | ArrowDown) --------------------------
|
||||
if (event.keyCode === 38 && window.__cmd_index > 0) { // ArrowUp
|
||||
window.__cmd_list[window.__cmd_index].classList.remove('selected');
|
||||
window.__cmd_index = window.__cmd_index - 1;
|
||||
window.__cmd_list[window.__cmd_index].classList.add('selected');
|
||||
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__cmd_list[window.__cmd_index].getAttribute('data-prompt'));
|
||||
searchInput.value = `/${window.__cmd_list[window.__cmd_index].getAttribute('data-cmd')}`;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.keyCode === 40 && window.__cmd_index < window.__cmd_list.length - 1) { // ArrowDown
|
||||
window.__cmd_list[window.__cmd_index].classList.remove('selected');
|
||||
window.__cmd_index = window.__cmd_index + 1;
|
||||
window.__cmd_list[window.__cmd_index].classList.add('selected');
|
||||
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__cmd_list[window.__cmd_index].getAttribute('data-prompt'));
|
||||
searchInput.value = `/${window.__cmd_list[window.__cmd_index].getAttribute('data-cmd')}`;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
const containerHeight = modelDom.offsetHeight;
|
||||
const itemHeight = window.__cmd_list[0].offsetHeight + 1;
|
||||
|
||||
const itemTop = window.__cmd_list[window.__cmd_index].offsetTop;
|
||||
const itemBottom = itemTop + itemHeight;
|
||||
if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) {
|
||||
modelDom.scrollTop = itemTop;
|
||||
}
|
||||
|
||||
// ------------------ TAB key replaces `{q}` tag content -------------------------------
|
||||
// feat: https://github.com/lencx/ChatGPT/issues/54
|
||||
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
|
||||
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
|
||||
|
||||
if (strGroup[1]) {
|
||||
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
|
||||
window.__CHAT_MODEL_STATUS__ = 1;
|
||||
} else {
|
||||
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
initDom();
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB
|
||||
const data = searchInput.value.split('|->');
|
||||
if (data[1]?.trim()) {
|
||||
setPrompt(data[1]);
|
||||
window.__CHAT_MODEL_STATUS__ = 2;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// input text
|
||||
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB
|
||||
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
modelDom.innerHTML = '';
|
||||
delete window.__CHAT_MODEL_STATUS__;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// ------------------ type in a space to complete the fill ------------------------------------
|
||||
if (event.keyCode === 32) {
|
||||
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
modelDom.innerHTML = '';
|
||||
delete window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
}
|
||||
|
||||
// ------------------ send --------------------------------------------------------------------
|
||||
if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter
|
||||
const data = searchInput.value.split('|->');
|
||||
setPrompt(data[1]);
|
||||
|
||||
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
|
||||
initDom();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
searchInput.removeEventListener('keydown', cmdKeydown, { capture: true });
|
||||
searchInput.addEventListener('keydown', cmdKeydown, { capture: true });
|
||||
|
||||
function cmdInput() {
|
||||
if (searchInput.value === '') {
|
||||
initDom();
|
||||
}
|
||||
|
||||
if (window.__CHAT_MODEL_STATUS__) return;
|
||||
|
||||
const query = searchInput.value;
|
||||
if (!query || !/^\//.test(query)) {
|
||||
initDom();
|
||||
return;
|
||||
}
|
||||
|
||||
// all cmd result
|
||||
if (query === '/') {
|
||||
renderList(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
|
||||
if (result.length > 0) {
|
||||
renderList(result);
|
||||
} else {
|
||||
initDom();
|
||||
}
|
||||
}
|
||||
searchInput.removeEventListener('input', cmdInput);
|
||||
searchInput.addEventListener('input', cmdInput);
|
||||
}
|
||||
}
|
||||
|
||||
function initDom() {
|
||||
const modelDom = document.querySelector('.chat-model-cmd-list');
|
||||
if (modelDom) {
|
||||
modelDom.innerHTML = '';
|
||||
}
|
||||
delete window.__CHAT_MODEL_CMD_PROMPT__;
|
||||
delete window.__CHAT_MODEL_CMD__;
|
||||
delete window.__CHAT_MODEL_STATUS__;
|
||||
delete window.__cmd_list;
|
||||
delete window.__cmd_index;
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
180
src-tauri/src/scripts/core.js
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
// *** Core Script - IPC ***
|
||||
|
||||
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||
function transformCallback(callback = () => {}, once = false) {
|
||||
const identifier = uid();
|
||||
const prop = `_${identifier}`;
|
||||
Object.defineProperty(window, prop, {
|
||||
value: (result) => {
|
||||
if (once) {
|
||||
Reflect.deleteProperty(window, prop);
|
||||
}
|
||||
return callback(result)
|
||||
},
|
||||
writable: false,
|
||||
configurable: true,
|
||||
})
|
||||
return identifier;
|
||||
}
|
||||
async function invoke(cmd, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
|
||||
const callback = transformCallback((e) => {
|
||||
resolve(e);
|
||||
Reflect.deleteProperty(window, `_${error}`);
|
||||
}, true)
|
||||
const error = transformCallback((e) => {
|
||||
reject(e);
|
||||
Reflect.deleteProperty(window, `_${callback}`);
|
||||
}, true)
|
||||
window.__TAURI_POST_MESSAGE__({
|
||||
cmd,
|
||||
callback,
|
||||
error,
|
||||
...args
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.uid = uid;
|
||||
window.invoke = invoke;
|
||||
window.transformCallback = transformCallback;
|
||||
|
||||
async function init() {
|
||||
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
|
||||
}
|
||||
|
||||
async function platform() {
|
||||
return invoke('platform', {
|
||||
__tauriModule: 'Os',
|
||||
message: { cmd: 'platform' }
|
||||
});
|
||||
}
|
||||
|
||||
if (__TAURI_METADATA__.__currentWindow.label !== 'tray') {
|
||||
const _platform = await platform();
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (/darwin/.test(_platform) && !chatConf.titlebar) {
|
||||
const topStyleDom = document.createElement("style");
|
||||
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
|
||||
document.head.appendChild(topStyleDom);
|
||||
const topDom = document.createElement("div");
|
||||
topDom.id = "chatgpt-app-window-top";
|
||||
document.body.appendChild(topDom);
|
||||
|
||||
if (window.location.host === 'chat.openai.com') {
|
||||
const nav = document.body.querySelector('nav');
|
||||
if (nav) {
|
||||
const currentPaddingTop = parseInt(window.getComputedStyle(document.querySelector('nav'), null).getPropertyValue('padding-top').replace('px', ''), 10);
|
||||
const navStyleDom = document.createElement("style");
|
||||
navStyleDom.innerHTML = `nav{padding-top:${currentPaddingTop + topDom.clientHeight}px !important}`;
|
||||
document.head.appendChild(navStyleDom);
|
||||
}
|
||||
}
|
||||
|
||||
topDom.addEventListener("mousedown", () => invoke("drag_window"));
|
||||
topDom.addEventListener("touchstart", () => invoke("drag_window"));
|
||||
topDom.addEventListener("dblclick", () => invoke("fullscreen"));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const origin = e.target.closest("a");
|
||||
if (!origin || !origin.target) return;
|
||||
if (origin && origin.href && origin.target !== '_self') {
|
||||
invoke('open_link', { url: origin.href });
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('wheel', function(event) {
|
||||
const deltaX = event.wheelDeltaX;
|
||||
if (Math.abs(deltaX) >= 50) {
|
||||
if (deltaX > 0) {
|
||||
window.history.go(-1);
|
||||
} else {
|
||||
window.history.go(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (window.location.host === 'chat.openai.com') {
|
||||
window.__sync_prompts = async function() {
|
||||
await invoke('sync_prompts', { time: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
coreZoom();
|
||||
}
|
||||
|
||||
function coreZoom() {
|
||||
const styleDom = document.createElement('style');
|
||||
styleDom.innerHTML = `
|
||||
#ZoomTopTip {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
background: #2a2a2a;
|
||||
color: #fafafa;
|
||||
padding: 20px 15px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
z-index: 999999;
|
||||
box-shadow: 0 2px 2px 2px #d8d8d8;
|
||||
}
|
||||
.ZoomTopTipAni {
|
||||
transition: opacity 200ms, display 200ms;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
document.head.append(styleDom);
|
||||
const zoomTipDom = document.createElement('div');
|
||||
zoomTipDom.id = 'ZoomTopTip';
|
||||
document.body.appendChild(zoomTipDom);
|
||||
function zoom(callback) {
|
||||
if (window.zoomSetTimeout) clearTimeout(window.zoomSetTimeout);
|
||||
const htmlZoom = window.localStorage.getItem("htmlZoom") || "100%";
|
||||
const html = document.getElementsByTagName("html")[0];
|
||||
const zoom = callback(htmlZoom);
|
||||
html.style.zoom = zoom;
|
||||
window.localStorage.setItem("htmlZoom", zoom);
|
||||
zoomTipDom.innerHTML = zoom;
|
||||
zoomTipDom.style.display = 'block';
|
||||
zoomTipDom.classList.remove('ZoomTopTipAni');
|
||||
window.zoomSetTimeout = setTimeout(() => {
|
||||
zoomTipDom.classList.add('ZoomTopTipAni');
|
||||
}, 2500);
|
||||
}
|
||||
function zoomDefault() {
|
||||
const htmlZoom = window.localStorage.getItem("htmlZoom");
|
||||
if (htmlZoom) {
|
||||
document.getElementsByTagName("html")[0].style.zoom = htmlZoom;
|
||||
}
|
||||
}
|
||||
function zoomIn() {
|
||||
zoom((htmlZoom) => `${Math.min(parseInt(htmlZoom) + 10, 200)}%`);
|
||||
}
|
||||
function zoomOut() {
|
||||
zoom((htmlZoom) => `${Math.max(parseInt(htmlZoom) - 10, 30)}%`);
|
||||
}
|
||||
function zoom0() {
|
||||
zoom(() => `100%`);
|
||||
}
|
||||
zoomDefault();
|
||||
window.__zoomIn = zoomIn;
|
||||
window.__zoomOut = zoomOut;
|
||||
window.__zoom0 = zoom0;
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
40
src-tauri/src/scripts/dalle2.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// *** Core Script - DALL·E 2 ***
|
||||
|
||||
function init() {
|
||||
document.addEventListener("click", (e) => {
|
||||
const origin = e.target.closest("a");
|
||||
if (!origin || !origin.target) return;
|
||||
if (origin && origin.href && origin.target !== '_self') {
|
||||
if (/\/(login|signup)$/.test(window.location.href)) {
|
||||
origin.target = '_self';
|
||||
} else {
|
||||
invoke('open_link', { url: origin.href });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (window.searchInterval) {
|
||||
clearInterval(window.searchInterval);
|
||||
}
|
||||
|
||||
window.searchInterval = setInterval(() => {
|
||||
const searchInput = document.querySelector('.image-prompt-form-wrapper form>.text-input');
|
||||
if (searchInput) {
|
||||
clearInterval(window.searchInterval);
|
||||
|
||||
if (!window.__CHATGPT_QUERY__) return;
|
||||
const query = decodeURIComponent(window.__CHATGPT_QUERY__);
|
||||
searchInput.focus();
|
||||
searchInput.value = query;
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
282
src-tauri/src/scripts/export.js
vendored
Normal file
@@ -0,0 +1,282 @@
|
||||
// *** Core Script - Export ***
|
||||
|
||||
async function init() {
|
||||
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
|
||||
if (window.innerWidth < 767) return;
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (window.buttonsInterval) {
|
||||
clearInterval(window.buttonsInterval);
|
||||
}
|
||||
window.buttonsInterval = setInterval(() => {
|
||||
const actionsArea = document.querySelector("form>div>div");
|
||||
if (!actionsArea) {
|
||||
return;
|
||||
}
|
||||
if (shouldAddButtons(actionsArea)) {
|
||||
let TryAgainButton = actionsArea.querySelector("button");
|
||||
if (!TryAgainButton) {
|
||||
const parentNode = document.createElement("div");
|
||||
parentNode.innerHTML = buttonOuterHTMLFallback;
|
||||
TryAgainButton = parentNode.querySelector("button");
|
||||
}
|
||||
addActionsButtons(actionsArea, TryAgainButton, chatConf);
|
||||
} else if (shouldRemoveButtons()) {
|
||||
removeButtons();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const Format = {
|
||||
PNG: "png",
|
||||
PDF: "pdf",
|
||||
};
|
||||
|
||||
function shouldRemoveButtons() {
|
||||
const isOpenScreen = document.querySelector("h1.text-4xl");
|
||||
if(isOpenScreen){
|
||||
return true;
|
||||
}
|
||||
const inConversation = document.querySelector("form button>div");
|
||||
if(inConversation){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldAddButtons(actionsArea) {
|
||||
// first, check if there's a "Try Again" button and no other buttons
|
||||
const buttons = actionsArea.querySelectorAll("button");
|
||||
|
||||
const hasTryAgainButton = Array.from(buttons).some((button) => {
|
||||
return !/download-/.test(button.id);
|
||||
});
|
||||
|
||||
const stopBtn = buttons?.[0]?.innerText;
|
||||
|
||||
if (/Stop generating/ig.test(stopBtn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasTryAgainButton && buttons.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise, check if open screen is not visible
|
||||
const isOpenScreen = document.querySelector("h1.text-4xl");
|
||||
if (isOpenScreen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the conversation is finished and there are no share buttons
|
||||
const finishedConversation = document.querySelector("form button>svg");
|
||||
const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]");
|
||||
if (finishedConversation && !hasShareButtons.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeButtons() {
|
||||
const downloadButton = document.getElementById("download-png-button");
|
||||
const downloadPdfButton = document.getElementById("download-pdf-button");
|
||||
const downloadMdButton = document.getElementById("download-markdown-button");
|
||||
if (downloadButton) {
|
||||
downloadButton.remove();
|
||||
}
|
||||
if (downloadPdfButton) {
|
||||
downloadPdfButton.remove();
|
||||
}
|
||||
if (downloadPdfButton) {
|
||||
downloadMdButton.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addActionsButtons(actionsArea, TryAgainButton) {
|
||||
const downloadButton = TryAgainButton.cloneNode(true);
|
||||
// Export markdown
|
||||
const exportMd = TryAgainButton.cloneNode(true);
|
||||
exportMd.id = "download-markdown-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
exportMd.title = "Export Markdown";
|
||||
exportMd.innerHTML = setIcon('md');
|
||||
exportMd.onclick = () => {
|
||||
exportMarkdown();
|
||||
};
|
||||
actionsArea.appendChild(exportMd);
|
||||
|
||||
// Generate PNG
|
||||
downloadButton.id = "download-png-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
downloadButton.title = "Generate PNG";
|
||||
downloadButton.innerHTML = setIcon('png');
|
||||
downloadButton.onclick = () => {
|
||||
downloadThread();
|
||||
};
|
||||
actionsArea.appendChild(downloadButton);
|
||||
|
||||
// Generate PDF
|
||||
const downloadPdfButton = TryAgainButton.cloneNode(true);
|
||||
downloadPdfButton.id = "download-pdf-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
downloadPdfButton.title = "Download PDF";
|
||||
downloadPdfButton.innerHTML = setIcon('pdf');
|
||||
downloadPdfButton.onclick = () => {
|
||||
downloadThread({ as: Format.PDF });
|
||||
};
|
||||
actionsArea.appendChild(downloadPdfButton);
|
||||
}
|
||||
|
||||
async function exportMarkdown() {
|
||||
const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML);
|
||||
const { id, filename } = getName();
|
||||
await invoke('save_file', { name: `notes/${id}.md`, content: data });
|
||||
await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' });
|
||||
}
|
||||
|
||||
function downloadThread({ as = Format.PNG } = {}) {
|
||||
const elements = new Elements();
|
||||
elements.fixLocation();
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
const minRatio = as === Format.PDF ? 2 : 2.5;
|
||||
window.devicePixelRatio = Math.max(pixelRatio, minRatio);
|
||||
|
||||
html2canvas(elements.thread, {
|
||||
letterRendering: true,
|
||||
}).then(async function (canvas) {
|
||||
elements.restoreLocation();
|
||||
window.devicePixelRatio = pixelRatio;
|
||||
const imgData = canvas.toDataURL("image/png");
|
||||
requestAnimationFrame(() => {
|
||||
if (as === Format.PDF) {
|
||||
return handlePdf(imgData, canvas, pixelRatio);
|
||||
} else {
|
||||
handleImg(imgData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleImg(imgData) {
|
||||
const binaryData = atob(imgData.split("base64,")[1]);
|
||||
const data = [];
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
data.push(binaryData.charCodeAt(i));
|
||||
}
|
||||
const { pathname, id, filename } = getName();
|
||||
await invoke('download', { name: `download/img/${id}.png`, blob: data });
|
||||
await invoke('download_list', { pathname, filename, id, dir: 'download' });
|
||||
}
|
||||
|
||||
async function handlePdf(imgData, canvas, pixelRatio) {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const orientation = canvas.width > canvas.height ? "l" : "p";
|
||||
var pdf = new jsPDF(orientation, "pt", [
|
||||
canvas.width / pixelRatio,
|
||||
canvas.height / pixelRatio,
|
||||
]);
|
||||
var pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
var pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST');
|
||||
const { pathname, id, filename } = getName();
|
||||
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
|
||||
await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) });
|
||||
await invoke('download_list', { pathname, filename, id, dir: 'download' });
|
||||
}
|
||||
|
||||
function getName() {
|
||||
const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
|
||||
const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || '';
|
||||
return { filename: name ? name : id, id, pathname: 'chat.download.json' };
|
||||
}
|
||||
|
||||
class Elements {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
// this.threadWrapper = document.querySelector(".cdfdFe");
|
||||
this.spacer = document.querySelector(".w-full.h-48.flex-shrink-0");
|
||||
this.thread = document.querySelector(
|
||||
"[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div"
|
||||
);
|
||||
|
||||
// fix: old chat https://github.com/lencx/ChatGPT/issues/185
|
||||
if (!this.thread) {
|
||||
this.thread = document.querySelector("main .overflow-y-auto");
|
||||
}
|
||||
|
||||
// h-full overflow-y-auto
|
||||
this.positionForm = document.querySelector("form").parentNode;
|
||||
// this.styledThread = document.querySelector("main");
|
||||
// this.threadContent = document.querySelector(".gAnhyd");
|
||||
this.scroller = Array.from(
|
||||
document.querySelectorAll('[class*="react-scroll-to"]')
|
||||
).filter((el) => el.classList.contains("h-full"))[0];
|
||||
|
||||
// fix: old chat
|
||||
if (!this.scroller) {
|
||||
this.scroller = document.querySelector('main .overflow-y-auto');
|
||||
}
|
||||
|
||||
this.hiddens = Array.from(document.querySelectorAll(".overflow-hidden"));
|
||||
this.images = Array.from(document.querySelectorAll("img[srcset]"));
|
||||
}
|
||||
fixLocation() {
|
||||
this.hiddens.forEach((el) => {
|
||||
el.classList.remove("overflow-hidden");
|
||||
});
|
||||
this.spacer.style.display = "none";
|
||||
this.thread.style.maxWidth = "960px";
|
||||
this.thread.style.marginInline = "auto";
|
||||
this.positionForm.style.display = "none";
|
||||
this.scroller.classList.remove("h-full");
|
||||
this.scroller.style.minHeight = "100vh";
|
||||
this.images.forEach((img) => {
|
||||
const srcset = img.getAttribute("srcset");
|
||||
img.setAttribute("srcset_old", srcset);
|
||||
img.setAttribute("srcset", "");
|
||||
});
|
||||
//Fix to the text shifting down when generating the canvas
|
||||
document.body.style.lineHeight = "0.5";
|
||||
}
|
||||
restoreLocation() {
|
||||
this.hiddens.forEach((el) => {
|
||||
el.classList.add("overflow-hidden");
|
||||
});
|
||||
this.spacer.style.display = null;
|
||||
this.thread.style.maxWidth = null;
|
||||
this.thread.style.marginInline = null;
|
||||
this.positionForm.style.display = null;
|
||||
this.scroller.classList.add("h-full");
|
||||
this.scroller.style.minHeight = null;
|
||||
this.images.forEach((img) => {
|
||||
const srcset = img.getAttribute("srcset_old");
|
||||
img.setAttribute("srcset", srcset);
|
||||
img.setAttribute("srcset_old", "");
|
||||
});
|
||||
document.body.style.lineHeight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setIcon(type) {
|
||||
return {
|
||||
// link: `<svg class="chatappico" viewBox="0 0 1024 1024"><path d="M1007.382 379.672L655.374 75.702C624.562 49.092 576 70.694 576 112.03v160.106C254.742 275.814 0 340.2 0 644.652c0 122.882 79.162 244.618 166.666 308.264 27.306 19.862 66.222-5.066 56.154-37.262C132.132 625.628 265.834 548.632 576 544.17V720c0 41.4 48.6 62.906 79.374 36.328l352.008-304c22.142-19.124 22.172-53.506 0-72.656z" p-id="8506" fill="currentColor"></path></svg>`,
|
||||
png: `<svg class="chatappico" viewBox="0 0 1070 1024"><path d="M981.783273 0H85.224727C38.353455 0 0 35.374545 0 83.083636v844.893091c0 47.616 38.353455 86.574545 85.178182 86.574546h903.633454c46.917818 0 81.733818-38.958545 81.733819-86.574546V83.083636C1070.592 35.374545 1028.701091 0 981.783273 0zM335.825455 135.912727c74.193455 0 134.330182 60.974545 134.330181 136.285091 0 75.170909-60.136727 136.192-134.330181 136.192-74.286545 0-134.516364-61.021091-134.516364-136.192 0-75.264 60.229818-136.285091 134.516364-136.285091z m-161.512728 745.937455a41.890909 41.890909 0 0 1-27.648-10.379637 43.752727 43.752727 0 0 1-4.654545-61.067636l198.097454-255.162182a42.123636 42.123636 0 0 1 57.716364-6.702545l116.549818 128.139636 286.906182-352.814545c14.615273-18.711273 90.251636-106.775273 135.866182-6.935273 0.093091-0.093091 0.093091 112.965818 0.232727 247.761455 0.093091 140.8 0.093091 317.067636 0.093091 317.067636-1.024-0.093091-762.740364 0.093091-763.112727 0.093091z" fill="currentColor"></path></svg>`,
|
||||
pdf: `<svg class="chatappico pdf" viewBox="0 0 1024 1024"><path d="M821.457602 118.382249H205.725895c-48.378584 0-87.959995 39.583368-87.959996 87.963909v615.731707c0 48.378584 39.581411 87.959995 87.959996 87.959996h615.733664c48.380541 0 87.961952-39.581411 87.961952-87.959996V206.346158c-0.001957-48.378584-39.583368-87.963909-87.963909-87.963909zM493.962468 457.544987c-10.112054 32.545237-21.72487 82.872662-38.806571 124.248336-8.806957 22.378397-8.380404 18.480717-15.001764 32.609808l5.71738-1.851007c58.760658-16.443827 99.901532-20.519564 138.162194-27.561607-7.67796-6.06371-14.350194-10.751884-19.631237-15.586807-26.287817-29.101504-35.464584-34.570387-70.440002-111.862636v0.003913z m288.36767 186.413594c-7.476424 8.356924-20.670227 13.191847-40.019704 13.191847-33.427694 0-63.808858-9.229597-107.79277-31.660824-75.648648 8.356924-156.097 17.214754-201.399704 31.729308-2.199293 0.876587-4.832967 1.759043-7.916674 3.077836-54.536215 93.237125-95.031389 132.767663-130.621199 131.19646-11.286054-0.49895-27.694661-7.044-32.973748-10.11988l-6.52157-6.196764-2.29517-4.353583c-3.07588-7.91863-3.954423-15.395054-2.197337-23.751977 4.838837-23.309771 29.907651-60.251638 82.686779-93.237126 8.356924-6.159587 27.430511-15.897917 45.020944-24.25484 13.311204-21.177004 19.45905-34.744531 36.341171-72.259702 19.102937-45.324228 36.505531-99.492589 47.500041-138.191543v-0.44025c-16.267727-53.219378-25.945401-89.310095-9.67376-147.80856 3.958337-16.71189 18.46702-33.864031 34.748444-33.864031h10.552304c10.115967 0 19.791684 3.520043 26.829814 10.552304 29.029107 29.031064 15.39114 103.824649 0.8805 162.323113-0.8805 2.63563-1.322707 4.832967-1.761 6.153717 17.59239 49.697378 45.400538 98.774492 73.108895 121.647926 11.436717 8.791304 22.638634 18.899444 36.71098 26.814161 19.791684-2.20125 37.517128-4.11487 55.547812-4.11487 54.540128 0 87.525615 9.67963 100.279169 30.351814 4.400543 7.034217 6.595923 15.389184 5.281043 24.1844-0.44025 10.996467-4.39663 21.112434-12.31526 29.031064z m-27.796407-36.748157c-4.394673-4.398587-17.024957-16.936907-78.601259-16.936907-3.073923 0-10.622744-0.784623-14.57521 3.612007 32.104987 14.072347 62.830525 24.757704 83.058545 24.757703 3.083707 0 5.72325-0.442207 8.356923-0.876586h1.759044c2.20125-0.8805 3.520043-1.324663 3.960293-5.71738-0.87463-1.324663-1.757087-3.083707-3.958336-4.838837z m-387.124553 63.041845c-9.237424 5.27713-16.71189 10.112054-21.112433 13.634053-31.226444 28.586901-51.018128 57.616008-53.217422 74.331812 19.789727-6.59788 45.737084-35.626987 74.329855-87.961952v-0.003913z m125.574957-297.822284l2.197336-1.761c3.079793-14.072347 5.232127-29.189554 7.87167-38.869184l1.318794-7.036174c4.39663-25.070771 2.71781-39.720334-4.76057-50.272637l-6.59788-2.20125a57.381208 57.381208 0 0 0-3.079794 5.27713c-7.474467 18.47289-7.063567 55.283661 3.0524 94.865072l-0.001956-0.001957z" fill="currentColor"></path></svg>`,
|
||||
md: `<svg class="chatappico md" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1380" width="200" height="200"><path d="M128 128h768a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667z m170.666667 533.333333v-170.666666l85.333333 85.333333 85.333333-85.333333v170.666666h85.333334v-298.666666h-85.333334l-85.333333 85.333333-85.333333-85.333333H213.333333v298.666666h85.333334z m469.333333-128v-170.666666h-85.333333v170.666666h-85.333334l128 128 128-128h-85.333333z" p-id="1381" fill="currentColor"></path></svg>`
|
||||
}[type];
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
36
src-tauri/src/scripts/markdown.export.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
var ExportMD = (function () {
|
||||
if (!TurndownService || !turndownPluginGfm) return;
|
||||
const hljsREG = /^.*(hljs).*(language-[a-z0-9]+).*$/i;
|
||||
const gfm = turndownPluginGfm.gfm
|
||||
const turndownService = new TurndownService()
|
||||
.use(gfm)
|
||||
.addRule('code', {
|
||||
filter: (node) => {
|
||||
if (node.nodeName === 'CODE' && hljsREG.test(node.classList.value)) {
|
||||
return 'code';
|
||||
}
|
||||
},
|
||||
replacement: (content, node) => {
|
||||
const classStr = node.getAttribute('class');
|
||||
if (hljsREG.test(classStr)) {
|
||||
const lang = classStr.match(/.*language-(\w+)/)[1];
|
||||
if (lang) {
|
||||
return `\`\`\`${lang}\n${content}\n\`\`\``;
|
||||
}
|
||||
return `\`\`\`\n${content}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
})
|
||||
.addRule('ignore', {
|
||||
filter: ['button', 'img'],
|
||||
replacement: () => '',
|
||||
})
|
||||
.addRule('table', {
|
||||
filter: 'table',
|
||||
replacement: function(content, node) {
|
||||
return `\`\`\`${content}\n\`\`\``;
|
||||
},
|
||||
});
|
||||
|
||||
return turndownService;
|
||||
}({}));
|
||||
84
src-tauri/src/scripts/popup.core.js
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// *** Core Script - DALL·E 2 Core ***
|
||||
|
||||
async function init() {
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (!chatConf.popup_search) return;
|
||||
if (!window.FloatingUIDOM) return;
|
||||
|
||||
const styleDom = document.createElement('style');
|
||||
styleDom.innerHTML = `
|
||||
#chagpt-selection-menu {
|
||||
display: none;
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #4a4a4a;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 3px 5px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
document.head.append(styleDom);
|
||||
|
||||
const selectionMenu = document.createElement('div');
|
||||
selectionMenu.id = 'chagpt-selection-menu';
|
||||
selectionMenu.innerHTML = 'DALL·E 2';
|
||||
document.body.appendChild(selectionMenu);
|
||||
const { computePosition, flip, offset, shift } = window.FloatingUIDOM;
|
||||
|
||||
document.body.addEventListener('mousedown', async (e) => {
|
||||
selectionMenu.style.display = 'none';
|
||||
if (e.target.id === 'chagpt-selection-menu') {
|
||||
await invoke('dalle2_search_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) });
|
||||
} else {
|
||||
delete window.__DALLE2_CONTENT__;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("mouseup", async (e) => {
|
||||
selectionMenu.style.display = 'none';
|
||||
const selection = window.getSelection();
|
||||
window.__DALLE2_CONTENT__ = selection.toString().trim();
|
||||
|
||||
if (!window.__DALLE2_CONTENT__) return;
|
||||
|
||||
if (selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getClientRects()[0];
|
||||
|
||||
const rootEl = document.createElement('div');
|
||||
rootEl.style.top = `${rect.top}px`;
|
||||
rootEl.style.position = 'fixed';
|
||||
rootEl.style.left = `${rect.left}px`;
|
||||
document.body.appendChild(rootEl);
|
||||
|
||||
selectionMenu.style.display = 'block';
|
||||
computePosition(rootEl, selectionMenu, {
|
||||
placement: 'top',
|
||||
middleware: [
|
||||
flip(),
|
||||
offset(5),
|
||||
shift({ padding: 5 })
|
||||
]
|
||||
}).then(({x, y}) => {
|
||||
Object.assign(selectionMenu.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
@@ -1,83 +1,275 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
use tauri::Manager;
|
||||
// use tauri::utils::config::Config;
|
||||
use tauri::updater::UpdateResponse;
|
||||
use tauri::{utils::config::Config, AppHandle, Manager, Wry};
|
||||
|
||||
pub fn chat_root() -> PathBuf {
|
||||
tauri::api::path::home_dir().unwrap().join(".chatgpt")
|
||||
tauri::api::path::home_dir().unwrap().join(".chatgpt")
|
||||
}
|
||||
|
||||
// pub fn get_tauri_conf() -> Option<Config> {
|
||||
// let config_file = include_str!("../tauri.conf.json");
|
||||
// let config: Config =
|
||||
// serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
|
||||
// Some(config)
|
||||
// }
|
||||
pub fn get_tauri_conf() -> Option<Config> {
|
||||
let config_file = include_str!("../tauri.conf.json");
|
||||
let config: Config = serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub fn exists(path: &Path) -> bool {
|
||||
Path::new(path).exists()
|
||||
Path::new(path).exists()
|
||||
}
|
||||
|
||||
pub fn create_file(path: &Path) -> Result<File> {
|
||||
if let Some(p) = path.parent() {
|
||||
fs::create_dir_all(p)?
|
||||
}
|
||||
File::create(path).map_err(Into::into)
|
||||
if let Some(p) = path.parent() {
|
||||
fs::create_dir_all(p)?
|
||||
}
|
||||
File::create(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn create_chatgpt_prompts() {
|
||||
let sync_file = chat_root().join("cache_model").join("chatgpt_prompts.json");
|
||||
if !exists(&sync_file) {
|
||||
create_file(&sync_file).unwrap();
|
||||
fs::write(&sync_file, "[]").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn script_path() -> PathBuf {
|
||||
let script_file = chat_root().join("main.js");
|
||||
if !exists(&script_file) {
|
||||
create_file(&script_file).unwrap();
|
||||
fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap();
|
||||
}
|
||||
let script_file = chat_root().join("main.js");
|
||||
if !exists(&script_file) {
|
||||
create_file(&script_file).unwrap();
|
||||
fs::write(
|
||||
&script_file,
|
||||
format!(
|
||||
"// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');",
|
||||
&script_file.to_string_lossy()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
script_file
|
||||
script_file
|
||||
}
|
||||
|
||||
pub fn user_script() -> String {
|
||||
let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string());
|
||||
format!(
|
||||
"window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
|
||||
user_script_content
|
||||
)
|
||||
let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string());
|
||||
format!(
|
||||
"window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
|
||||
user_script_content
|
||||
)
|
||||
}
|
||||
|
||||
pub fn open_file(path: PathBuf) {
|
||||
info!("open_file: {}", path.to_string_lossy());
|
||||
#[cfg(target_os = "macos")]
|
||||
Command::new("open").arg("-R").arg(path).spawn().unwrap();
|
||||
let pathname = convert_path(path.to_str().unwrap());
|
||||
info!("open_file: {}", pathname);
|
||||
#[cfg(target_os = "macos")]
|
||||
Command::new("open")
|
||||
.arg("-R")
|
||||
.arg(pathname)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
Command::new("explorer")
|
||||
.arg("/select,")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
Command::new("explorer.exe")
|
||||
.arg("/select,")
|
||||
.arg(pathname)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
// https://askubuntu.com/a/31071
|
||||
#[cfg(target_os = "linux")]
|
||||
Command::new("xdg-open").arg(path).spawn().unwrap();
|
||||
// https://askubuntu.com/a/31071
|
||||
#[cfg(target_os = "linux")]
|
||||
Command::new("xdg-open").arg(pathname).spawn().unwrap();
|
||||
}
|
||||
|
||||
pub fn convert_path(path_str: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
path_str.replace('/', "\\")
|
||||
} else {
|
||||
String::from(path_str)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_conf(app: &tauri::AppHandle) {
|
||||
let root = chat_root();
|
||||
let app2 = app.clone();
|
||||
let msg = format!("Path: {}\nAre you sure to clear all ChatGPT configurations? Please backup in advance if necessary!", root.to_string_lossy());
|
||||
tauri::api::dialog::ask(
|
||||
app.get_window("core").as_ref(),
|
||||
"Clear Config",
|
||||
msg,
|
||||
move |is_ok| {
|
||||
if is_ok {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
tauri::api::process::restart(&app2.env());
|
||||
}
|
||||
},
|
||||
);
|
||||
let root = chat_root();
|
||||
let msg = format!(
|
||||
"Path: {}\n
|
||||
Are you sure you want to clear all ChatGPT configurations? Performing this operation data can not be restored, please back up in advance.\n
|
||||
Note: The application will exit automatically after the configuration cleanup!",
|
||||
root.to_string_lossy()
|
||||
);
|
||||
tauri::api::dialog::ask(
|
||||
app.get_window("core").as_ref(),
|
||||
"Clear Config",
|
||||
msg,
|
||||
move |is_ok| {
|
||||
if is_ok {
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value {
|
||||
match v {
|
||||
Value::Object(m) => {
|
||||
let mut m = m.clone();
|
||||
for (k, v) in fields {
|
||||
m.insert(k.clone(), v.clone());
|
||||
}
|
||||
Value::Object(m)
|
||||
}
|
||||
v => v.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_cmd(name: String) -> String {
|
||||
let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
|
||||
re.replace_all(&name, "_").to_lowercase()
|
||||
}
|
||||
|
||||
pub async fn get_data(
|
||||
url: &str,
|
||||
app: Option<&tauri::AppHandle>,
|
||||
) -> Result<Option<String>, reqwest::Error> {
|
||||
let res = reqwest::get(url).await?;
|
||||
let is_ok = res.status() == 200;
|
||||
let body = res.text().await?;
|
||||
|
||||
if is_ok {
|
||||
Ok(Some(body))
|
||||
} else {
|
||||
info!("chatgpt_http_error: {}", body);
|
||||
if let Some(v) = app {
|
||||
tauri::api::dialog::message(v.get_window("core").as_ref(), "ChatGPT HTTP", body);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>) {
|
||||
info!("run_check_update: silent={} has_msg={:?}", silent, has_msg);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let result = app.updater().check().await;
|
||||
let update_resp = result.unwrap();
|
||||
if update_resp.is_update_available() {
|
||||
if silent {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
silent_install(app, update_resp).await.unwrap();
|
||||
});
|
||||
} else {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
prompt_for_install(app, update_resp).await.unwrap();
|
||||
});
|
||||
}
|
||||
} else if let Some(v) = has_msg {
|
||||
if v {
|
||||
tauri::api::dialog::message(
|
||||
app.app_handle().get_window("core").as_ref(),
|
||||
"ChatGPT",
|
||||
"Your ChatGPT is up to date",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy private api in tauri/updater/mod.rs. TODO: refactor to public api
|
||||
// Prompt a dialog asking if the user want to install the new version
|
||||
// Maybe we should add an option to customize it in future versions.
|
||||
pub async fn prompt_for_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
|
||||
info!("prompt_for_install");
|
||||
let windows = app.windows();
|
||||
let parent_window = windows.values().next();
|
||||
let package_info = app.package_info().clone();
|
||||
|
||||
let body = update.body().unwrap();
|
||||
// todo(lemarier): We should review this and make sure we have
|
||||
// something more conventional.
|
||||
let should_install = tauri::api::dialog::blocking::ask(
|
||||
parent_window,
|
||||
format!(r#"A new version of {} is available! "#, package_info.name),
|
||||
format!(
|
||||
r#"{} {} is now available -- you have {}.
|
||||
|
||||
Would you like to install it now?
|
||||
|
||||
Release Notes:
|
||||
{}"#,
|
||||
package_info.name,
|
||||
update.latest_version(),
|
||||
package_info.version,
|
||||
body
|
||||
),
|
||||
);
|
||||
|
||||
if should_install {
|
||||
// Launch updater download process
|
||||
// macOS we display the `Ready to restart dialog` asking to restart
|
||||
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
|
||||
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
|
||||
update.download_and_install().await?;
|
||||
|
||||
// Ask user if we need to restart the application
|
||||
let should_exit = tauri::api::dialog::blocking::ask(
|
||||
parent_window,
|
||||
"Ready to Restart",
|
||||
"The installation was successful, do you want to restart the application now?",
|
||||
);
|
||||
if should_exit {
|
||||
app.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
|
||||
info!("silent_install");
|
||||
let windows = app.windows();
|
||||
let parent_window = windows.values().next();
|
||||
|
||||
// Launch updater download process
|
||||
// macOS we display the `Ready to restart dialog` asking to restart
|
||||
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
|
||||
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
|
||||
update.download_and_install().await?;
|
||||
|
||||
// Ask user if we need to restart the application
|
||||
let should_exit = tauri::api::dialog::blocking::ask(
|
||||
parent_window,
|
||||
"Ready to Restart",
|
||||
"The silent installation was successful, do you want to restart the application now?",
|
||||
);
|
||||
if should_exit {
|
||||
app.restart();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_hidden(entry: &walkdir::DirEntry) -> bool {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn vec_to_hashmap(
|
||||
vec: impl Iterator<Item = serde_json::Value>,
|
||||
key: &str,
|
||||
map: &mut HashMap<String, serde_json::Value>,
|
||||
) {
|
||||
for v in vec {
|
||||
if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
|
||||
map.insert(kval.to_string(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src-tauri/src/vendors/floating-ui-core.js
vendored
Normal file
1
src-tauri/src/vendors/floating-ui-dom.js
vendored
Normal file
164
src-tauri/src/vendors/turndown-plugin-gfm.js
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
var turndownPluginGfm = (function (exports) {
|
||||
'use strict';
|
||||
|
||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
||||
|
||||
function highlightedCodeBlock (turndownService) {
|
||||
turndownService.addRule('highlightedCodeBlock', {
|
||||
filter: function (node) {
|
||||
var firstChild = node.firstChild;
|
||||
return (
|
||||
node.nodeName === 'DIV' &&
|
||||
highlightRegExp.test(node.className) &&
|
||||
firstChild &&
|
||||
firstChild.nodeName === 'PRE'
|
||||
)
|
||||
},
|
||||
replacement: function (content, node, options) {
|
||||
var className = node.className || '';
|
||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
||||
|
||||
return (
|
||||
'\n\n' + options.fence + language + '\n' +
|
||||
node.firstChild.textContent +
|
||||
'\n' + options.fence + '\n\n'
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function strikethrough (turndownService) {
|
||||
turndownService.addRule('strikethrough', {
|
||||
filter: ['del', 's', 'strike'],
|
||||
replacement: function (content) {
|
||||
return '~' + content + '~'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var indexOf = Array.prototype.indexOf;
|
||||
var every = Array.prototype.every;
|
||||
var rules = {};
|
||||
|
||||
rules.tableCell = {
|
||||
filter: ['th', 'td'],
|
||||
replacement: function (content, node) {
|
||||
return cell(content, node)
|
||||
}
|
||||
};
|
||||
|
||||
rules.tableRow = {
|
||||
filter: 'tr',
|
||||
replacement: function (content, node) {
|
||||
var borderCells = '';
|
||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
||||
|
||||
if (isHeadingRow(node)) {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var border = '---';
|
||||
var align = (
|
||||
node.childNodes[i].getAttribute('align') || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (align) border = alignMap[align] || border;
|
||||
|
||||
borderCells += cell(border, node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
||||
}
|
||||
};
|
||||
|
||||
rules.table = {
|
||||
// Only convert tables with a heading row.
|
||||
// Tables with no heading row are kept using `keep` (see below).
|
||||
filter: function (node) {
|
||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
||||
},
|
||||
|
||||
replacement: function (content) {
|
||||
// Ensure there are no blank lines
|
||||
content = content.replace('\n\n', '\n');
|
||||
return '\n\n' + content + '\n\n'
|
||||
}
|
||||
};
|
||||
|
||||
rules.tableSection = {
|
||||
filter: ['thead', 'tbody', 'tfoot'],
|
||||
replacement: function (content) {
|
||||
return content
|
||||
}
|
||||
};
|
||||
|
||||
// A tr is a heading row if:
|
||||
// - the parent is a THEAD
|
||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
||||
// following a blank THEAD)
|
||||
// - and every cell is a TH
|
||||
function isHeadingRow (tr) {
|
||||
var parentNode = tr.parentNode;
|
||||
return (
|
||||
parentNode.nodeName === 'THEAD' ||
|
||||
(
|
||||
parentNode.firstChild === tr &&
|
||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function isFirstTbody (element) {
|
||||
var previousSibling = element.previousSibling;
|
||||
return (
|
||||
element.nodeName === 'TBODY' && (
|
||||
!previousSibling ||
|
||||
(
|
||||
previousSibling.nodeName === 'THEAD' &&
|
||||
/^\s*$/i.test(previousSibling.textContent)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function cell (content, node) {
|
||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
||||
var prefix = ' ';
|
||||
if (index === 0) prefix = '| ';
|
||||
return prefix + content + ' |'
|
||||
}
|
||||
|
||||
function tables (turndownService) {
|
||||
turndownService.keep(function (node) {
|
||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
||||
});
|
||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
||||
}
|
||||
|
||||
function taskListItems (turndownService) {
|
||||
turndownService.addRule('taskListItems', {
|
||||
filter: function (node) {
|
||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
||||
},
|
||||
replacement: function (content, node) {
|
||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function gfm (turndownService) {
|
||||
turndownService.use([
|
||||
highlightedCodeBlock,
|
||||
strikethrough,
|
||||
tables,
|
||||
taskListItems
|
||||
]);
|
||||
}
|
||||
|
||||
exports.gfm = gfm;
|
||||
exports.highlightedCodeBlock = highlightedCodeBlock;
|
||||
exports.strikethrough = strikethrough;
|
||||
exports.tables = tables;
|
||||
exports.taskListItems = taskListItems;
|
||||
|
||||
return exports;
|
||||
}({}));
|
||||
1
src-tauri/src/vendors/turndown.js
vendored
Normal file
@@ -7,20 +7,18 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "ChatGPT",
|
||||
"version": "0.5.0"
|
||||
"version": "0.10.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": true,
|
||||
"http": {
|
||||
"scope": [
|
||||
"https://raw.githubusercontent.com/*"
|
||||
]
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
},
|
||||
"fs": {
|
||||
"all": true,
|
||||
"scope": [
|
||||
"$HOME/.chatgpt/*"
|
||||
"$HOME/.chatgpt/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -62,12 +60,6 @@
|
||||
"webviewInstallMode": {
|
||||
"silent": true,
|
||||
"type": "embedBootstrapper"
|
||||
},
|
||||
"wix": {
|
||||
"language": [
|
||||
"zh-CN",
|
||||
"en-US"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -76,23 +68,11 @@
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://lencx.github.io/ChatGPT/install.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMjY4OUI5MTVFNjBEMDUKUldRRkRlWVZ1WWttc1NGWEE0RFNSb0RqdnhsekRJZTkwK2hVLzhBZTZnaHExSEZ1ZEdzWkpXTHkK"
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"url": "index.html",
|
||||
"title": "ChatGPT",
|
||||
"visible": false,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/components/FilePath/index.tsx
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { path, shell } from '@tauri-apps/api';
|
||||
|
||||
import { chatRoot } from '@/utils';
|
||||
|
||||
interface FilePathProps {
|
||||
paths?: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const FilePath: FC<FilePathProps> = ({ className, label = 'PATH', paths = '', url, content }) => {
|
||||
const [filePath, setPath] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!path && !url) return;
|
||||
(async () => {
|
||||
if (url) {
|
||||
setPath(url);
|
||||
return;
|
||||
}
|
||||
setPath(await path.join(await chatRoot(), ...paths.split('/').filter((i) => !!i)));
|
||||
})();
|
||||
}, [url, paths]);
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'chat-file-path')}>
|
||||
<div>
|
||||
{label}:{' '}
|
||||
<a onClick={() => shell.open(filePath)} title={filePath}>
|
||||
{content ? content : filePath}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePath;
|
||||
48
src/components/Markdown/Editor.tsx
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import Markdown from '@/components/Markdown';
|
||||
import './index.scss';
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
value?: string;
|
||||
onChange?: (v: string) => void;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
const MarkdownEditor: FC<MarkdownEditorProps> = ({ value = '', onChange, mode = 'split' }) => {
|
||||
const [content, setContent] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setContent(value);
|
||||
onChange && onChange(value);
|
||||
}, [value]);
|
||||
|
||||
const handleEdit = (e: any) => {
|
||||
setContent(e);
|
||||
onChange && onChange(e);
|
||||
};
|
||||
|
||||
const isSplit = mode === 'split';
|
||||
|
||||
return (
|
||||
<div className="md-main">
|
||||
<PanelGroup direction="horizontal">
|
||||
{['md', 'split'].includes(mode) && (
|
||||
<Panel>
|
||||
<Editor language="markdown" value={content} onChange={handleEdit} />
|
||||
</Panel>
|
||||
)}
|
||||
{isSplit && <PanelResizeHandle className="resize-handle" />}
|
||||
{['doc', 'split'].includes(mode) && (
|
||||
<Panel>
|
||||
<Markdown className="edit-preview">{content}</Markdown>
|
||||
</Panel>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
||||
33
src/components/Markdown/index.scss
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
.markdown-body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||
|
||||
&.edit-preview {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.md-main {
|
||||
height: calc(100vh - 130px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
width: 0.25rem;
|
||||
transition: 250ms linear background-color;
|
||||
background-color: #7f8082;
|
||||
outline: none;
|
||||
|
||||
&:hover,
|
||||
&[data-resize-handle-active] {
|
||||
background-color: #5194eb;
|
||||
}
|
||||
}
|
||||
52
src/components/Markdown/index.tsx
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import { FC } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import agate from 'react-syntax-highlighter/dist/esm/styles/hljs/agate';
|
||||
import 'github-markdown-css';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Markdown: FC<MarkdownProps> = ({ children, className }) => {
|
||||
return (
|
||||
<div className={clsx(className, 'markdown-body')}>
|
||||
<div>
|
||||
<ReactMarkdown
|
||||
children={children}
|
||||
linkTarget="_blank"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
style={agate as any}
|
||||
language={match[1]}
|
||||
showLineNumbers
|
||||
lineNumberStyle={{ color: '#999' }}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
98
src/components/SwitchOrigin/index.tsx
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Form, Select, Tag, Tooltip, Switch } from 'antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import useJson from '@/hooks/useJson';
|
||||
import { DISABLE_AUTO_COMPLETE, CHAT_AWESOME_JSON } from '@/utils';
|
||||
interface SwitchOriginProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const SwitchOrigin: FC<SwitchOriginProps> = ({ name }) => {
|
||||
const { json: list = [] } = useJson<any[]>(CHAT_AWESOME_JSON);
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const labelName = `(${name === 'main' ? 'Main' : 'SystemTray'})`;
|
||||
const dashboardName = `${name}_dashboard`;
|
||||
const originName = `${name}_origin`;
|
||||
const isEnable = Form.useWatch(dashboardName, form);
|
||||
|
||||
let urlList = [{ title: 'ChatGPT', url: 'https://chat.openai.com', init: true }];
|
||||
if (Array.isArray(list)) {
|
||||
urlList = urlList.concat(list);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Dashboard {labelName}{' '}
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<p>
|
||||
<b>Set Dashboard as the application default window.</b>
|
||||
</p>
|
||||
<p>
|
||||
If this is enabled, the <Tag color="blue">Switch Origin {labelName}</Tag>{' '}
|
||||
setting will be invalid.
|
||||
</p>
|
||||
<p>
|
||||
If you want to add a new URL to the dashboard, add it in the{' '}
|
||||
<Link to="/awesome">Awesome</Link> menu and make sure it is enabled.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={dashboardName}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Switch Origin {labelName}{' '}
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<p>
|
||||
<b>Set a single URL as the application default window.</b>
|
||||
</p>
|
||||
<p>
|
||||
If you need to set a new URL as the application loading window, please add the
|
||||
URL in the <Link to="/awesome">Awesome</Link> menu and then select it.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={originName}
|
||||
>
|
||||
<Select disabled={isEnable} showSearch {...DISABLE_AUTO_COMPLETE} optionLabelProp="url">
|
||||
{urlList.map((i, idx) => (
|
||||
<Select.Option
|
||||
key={`${idx}_${i.url}`}
|
||||
label={i.title}
|
||||
value={i.url}
|
||||
title={`${i.title}${i.init ? '(Built-in)' : ''}: ${i.url}`}
|
||||
>
|
||||
<Tag color={i.init ? 'orange' : 'geekblue'}>{i.title}</Tag> {i.url}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchOrigin;
|
||||
12
src/components/Tags/index.tsx
vendored
@@ -8,9 +8,11 @@ import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||
interface TagsProps {
|
||||
value?: string[];
|
||||
onChange?: (v: string[]) => void;
|
||||
addTxt?: string;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
|
||||
const Tags: FC<TagsProps> = ({ max = 99, value = [], onChange, addTxt = 'New Tag' }) => {
|
||||
const [tags, setTags] = useState<string[]>(value);
|
||||
const [inputVisible, setInputVisible] = useState<boolean>(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
@@ -18,7 +20,7 @@ const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
|
||||
|
||||
useEffect(() => {
|
||||
setTags(value);
|
||||
}, [value])
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputVisible) {
|
||||
@@ -86,13 +88,13 @@ const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
|
||||
{...DISABLE_AUTO_COMPLETE}
|
||||
/>
|
||||
)}
|
||||
{!inputVisible && (
|
||||
{!inputVisible && tags.length < max && (
|
||||
<Tag onClick={showInput} className="chat-tag-new">
|
||||
<PlusOutlined /> New Tag
|
||||
<PlusOutlined /> {addTxt}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
export default Tags;
|
||||
|
||||
53
src/hooks/useChatModel.ts
vendored
@@ -1,23 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { clone } from 'lodash';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import { CHAT_MODEL_JSON, readJSON, writeJSON } from '@/utils';
|
||||
import { CHAT_MODEL_JSON, CHAT_MODEL_CMD_JSON, readJSON, writeJSON } from '@/utils';
|
||||
import useInit from '@/hooks/useInit';
|
||||
|
||||
export default function useChatModel(key: string) {
|
||||
export default function useChatModel(key: string, file = CHAT_MODEL_JSON) {
|
||||
const [modelJson, setModelJson] = useState<Record<string, any>>({});
|
||||
|
||||
useInit(async () => {
|
||||
const data = await readJSON(CHAT_MODEL_JSON, { name: 'ChatGPT Model', [key]: [] });
|
||||
const data = await readJSON(file, {
|
||||
defaultVal: { name: 'ChatGPT Model', [key]: null },
|
||||
});
|
||||
setModelJson(data);
|
||||
});
|
||||
|
||||
const modelSet = async (data: Record<string, any>[]) => {
|
||||
const modelSet = async (data: Record<string, any>[] | Record<string, any>) => {
|
||||
const oData = clone(modelJson);
|
||||
oData[key] = data;
|
||||
await writeJSON(CHAT_MODEL_JSON, oData);
|
||||
await writeJSON(file, oData);
|
||||
setModelJson(oData);
|
||||
}
|
||||
};
|
||||
|
||||
return { modelJson, modelSet, modelData: modelJson?.[key] || [] }
|
||||
}
|
||||
return { modelJson, modelSet, modelData: modelJson?.[key] || [] };
|
||||
}
|
||||
|
||||
export function useCacheModel(file = '') {
|
||||
const [modelCacheJson, setModelCacheJson] = useState<Record<string, any>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) return;
|
||||
(async () => {
|
||||
const data = await readJSON(file, { isRoot: true, isList: true });
|
||||
setModelCacheJson(data);
|
||||
})();
|
||||
}, [file]);
|
||||
|
||||
const modelCacheSet = async (data: Record<string, any>[], newFile = '') => {
|
||||
await writeJSON(newFile ? newFile : file, data, { isRoot: true });
|
||||
setModelCacheJson(data);
|
||||
await modelCacheCmd();
|
||||
};
|
||||
|
||||
const modelCacheCmd = async () => {
|
||||
// Generate the `chat.model.cmd.json` file and refresh the page for the slash command to take effect.
|
||||
const list = await invoke('cmd_list');
|
||||
await writeJSON(CHAT_MODEL_CMD_JSON, {
|
||||
name: 'ChatGPT CMD',
|
||||
last_updated: Date.now(),
|
||||
data: list,
|
||||
});
|
||||
await invoke('window_reload', { label: 'core' });
|
||||
await invoke('window_reload', { label: 'tray' });
|
||||
};
|
||||
|
||||
return { modelCacheJson, modelCacheSet, modelCacheCmd };
|
||||
}
|
||||
|
||||
44
src/hooks/useColumns.ts
vendored
@@ -1,44 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export default function useColumns(columns: any[] = []) {
|
||||
const [opType, setOpType] = useState('');
|
||||
const [opRecord, setRecord] = useState<Record<string|symbol, any> | null>(null);
|
||||
const [opTime, setNow] = useState<number | null>(null);
|
||||
const [opExtra, setExtra] = useState<any>(null);
|
||||
|
||||
const handleRecord = useCallback((row: Record<string, any> | null, type: string) => {
|
||||
setOpType(type);
|
||||
setRecord(row);
|
||||
setNow(Date.now());
|
||||
}, []);
|
||||
|
||||
const resetRecord = useCallback(() => {
|
||||
setRecord(null);
|
||||
setOpType('');
|
||||
setNow(Date.now());
|
||||
}, []);
|
||||
|
||||
const opNew = useCallback(() => handleRecord(null, 'new'), [handleRecord]);
|
||||
|
||||
const cols = columns.map((i: any) => {
|
||||
if (i.render) {
|
||||
const opRender = i.render;
|
||||
i.render = (text: string, row: Record<string, any>) => {
|
||||
return opRender(text, row, { setRecord: handleRecord, setExtra });
|
||||
};
|
||||
}
|
||||
return i;
|
||||
});
|
||||
|
||||
return {
|
||||
opTime,
|
||||
opType,
|
||||
opNew,
|
||||
columns: cols,
|
||||
opRecord,
|
||||
setRecord: handleRecord,
|
||||
resetRecord,
|
||||
setExtra,
|
||||
opExtra,
|
||||
};
|
||||
}
|
||||
83
src/hooks/useColumns.tsx
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import { FC, useState, useCallback } from 'react';
|
||||
import { Input } from 'antd';
|
||||
|
||||
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||
|
||||
export default function useColumns(columns: any[] = []) {
|
||||
const [opType, setOpType] = useState('');
|
||||
const [opRecord, setRecord] = useState<Record<string | symbol, any> | null>(null);
|
||||
const [opTime, setNow] = useState<number | null>(null);
|
||||
const [opExtra, setExtra] = useState<any>(null);
|
||||
|
||||
const handleRecord = useCallback((row: Record<string, any> | null, type: string) => {
|
||||
setOpType(type);
|
||||
setRecord(row);
|
||||
setNow(Date.now());
|
||||
}, []);
|
||||
|
||||
const resetRecord = useCallback(() => {
|
||||
setRecord(null);
|
||||
setOpType('');
|
||||
setNow(Date.now());
|
||||
}, []);
|
||||
|
||||
const opNew = useCallback(() => handleRecord(null, 'new'), [handleRecord]);
|
||||
|
||||
const cols = columns.map((i: any) => {
|
||||
if (i.render) {
|
||||
const opRender = i.render;
|
||||
i.render = (text: string, row: Record<string, any>) => {
|
||||
return opRender(text, row, { setRecord: handleRecord, setExtra });
|
||||
};
|
||||
}
|
||||
return i;
|
||||
});
|
||||
|
||||
return {
|
||||
opTime,
|
||||
opType,
|
||||
opNew,
|
||||
columns: cols,
|
||||
opRecord,
|
||||
setRecord: handleRecord,
|
||||
resetRecord,
|
||||
setExtra,
|
||||
opExtra,
|
||||
};
|
||||
}
|
||||
|
||||
interface EditRowProps {
|
||||
rowKey: string;
|
||||
row: Record<string, any>;
|
||||
actions: any;
|
||||
}
|
||||
export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
|
||||
const [isEdit, setEdit] = useState(false);
|
||||
const [val, setVal] = useState(row[rowKey] || '');
|
||||
const handleEdit = () => {
|
||||
setEdit(true);
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setVal(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setEdit(false);
|
||||
row[rowKey] = val?.trim();
|
||||
actions?.setRecord(row, 'rowedit');
|
||||
};
|
||||
|
||||
return isEdit ? (
|
||||
<Input
|
||||
value={val}
|
||||
autoFocus
|
||||
onChange={handleChange}
|
||||
{...DISABLE_AUTO_COMPLETE}
|
||||
onPressEnter={handleSave}
|
||||
/>
|
||||
) : (
|
||||
<div className="rowedit" onClick={handleEdit}>
|
||||
{val}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
51
src/hooks/useData.ts
vendored
@@ -1,15 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const safeKey = Symbol('chat-id');
|
||||
export const safeKey = Symbol('chat-id');
|
||||
|
||||
export default function useData(oData: any[]) {
|
||||
const [opData, setData] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const nData = oData.map(i => ({ [safeKey]: v4(), ...i }));
|
||||
setData(nData);
|
||||
}, [oData])
|
||||
opInit(oData);
|
||||
}, []);
|
||||
|
||||
const opAdd = (val: any) => {
|
||||
const v = [val, ...opData];
|
||||
@@ -17,19 +16,55 @@ export default function useData(oData: any[]) {
|
||||
return v;
|
||||
};
|
||||
|
||||
const opInit = (val: any[] = []) => {
|
||||
if (!val || !Array.isArray(val)) return;
|
||||
const nData = val.map((i) => ({ [safeKey]: v4(), ...i }));
|
||||
setData(nData);
|
||||
};
|
||||
|
||||
const opRemove = (id: string) => {
|
||||
const nData = opData.filter(i => i[safeKey] !== id);
|
||||
const nData = opData.filter((i) => i[safeKey] !== id);
|
||||
setData(nData);
|
||||
return nData;
|
||||
};
|
||||
|
||||
const opRemoveItems = (ids: string[]) => {
|
||||
const nData = opData.filter((i) => !ids.includes(i[safeKey]));
|
||||
setData(nData);
|
||||
return nData;
|
||||
};
|
||||
|
||||
const opReplace = (id: string, data: any) => {
|
||||
const nData = [...opData];
|
||||
const idx = opData.findIndex(v => v[safeKey] === id);
|
||||
const idx = opData.findIndex((v) => v[safeKey] === id);
|
||||
nData[idx] = data;
|
||||
setData(nData);
|
||||
return nData;
|
||||
};
|
||||
|
||||
return { opSafeKey: safeKey, opReplace, opAdd, opRemove, opData };
|
||||
}
|
||||
const opReplaceItems = (ids: string[], data: any) => {
|
||||
const nData = [...opData];
|
||||
let count = 0;
|
||||
for (let i = 0; i < nData.length; i++) {
|
||||
const v = nData[i];
|
||||
if (ids.includes(v[safeKey])) {
|
||||
count++;
|
||||
nData[i] = { ...v, ...data };
|
||||
}
|
||||
if (count === ids.length) break;
|
||||
}
|
||||
setData(nData);
|
||||
return nData;
|
||||
};
|
||||
|
||||
return {
|
||||
opSafeKey: safeKey,
|
||||
opInit,
|
||||
opReplace,
|
||||
opAdd,
|
||||
opRemove,
|
||||
opRemoveItems,
|
||||
opData,
|
||||
opReplaceItems,
|
||||
};
|
||||
}
|
||||
|
||||
4
src/hooks/useInit.ts
vendored
@@ -8,5 +8,5 @@ export default function useInit(callback: () => void) {
|
||||
callback();
|
||||
isInit.current = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
23
src/hooks/useJson.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { readJSON, writeJSON } from '@/utils';
|
||||
import useInit from '@/hooks/useInit';
|
||||
|
||||
export default function useJson<T>(file: string) {
|
||||
const [json, setData] = useState<T>();
|
||||
|
||||
const refreshJson = async () => {
|
||||
const data = await readJSON(file);
|
||||
setData(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateJson = async (data: any) => {
|
||||
await writeJSON(file, data);
|
||||
await refreshJson();
|
||||
};
|
||||
|
||||
useInit(refreshJson);
|
||||
|
||||
return { json, refreshJson, updateJson };
|
||||
}
|
||||
57
src/hooks/useTable.tsx
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import type { TableRowSelection } from 'antd/es/table/interface';
|
||||
|
||||
import { safeKey } from '@/hooks/useData';
|
||||
|
||||
type rowSelectionOptions = {
|
||||
key: 'id' | string;
|
||||
rowType: 'id' | 'row' | 'all';
|
||||
};
|
||||
export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) {
|
||||
const { key = 'id', rowType = 'id' } = options;
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string | symbol, any>[]>([]);
|
||||
|
||||
const onSelectChange = (
|
||||
newSelectedRowKeys: React.Key[],
|
||||
newSelectedRows: Record<string | symbol, any>[],
|
||||
) => {
|
||||
const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]);
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
if (rowType === 'id') {
|
||||
setSelectedRowIDs(keys);
|
||||
}
|
||||
if (rowType === 'row') {
|
||||
setSelectedRows(newSelectedRows);
|
||||
}
|
||||
if (rowType === 'all') {
|
||||
setSelectedRowIDs(keys);
|
||||
setSelectedRows(newSelectedRows);
|
||||
}
|
||||
};
|
||||
|
||||
const rowReset = () => {
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRowIDs([]);
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const rowSelection: TableRowSelection<Record<string, any>> = {
|
||||
selectedRowKeys,
|
||||
onChange: onSelectChange,
|
||||
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE],
|
||||
};
|
||||
|
||||
return { rowSelection, selectedRowIDs, selectedRows, rowReset };
|
||||
}
|
||||
|
||||
export const TABLE_PAGINATION = {
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 10,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
showTotal: (total: number) => <span>Total {total} items</span>,
|
||||
};
|
||||
25
src/icons/SplitIcon.tsx
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import Icon from '@ant-design/icons';
|
||||
import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
|
||||
|
||||
interface IconProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function SplitIcon(props: Partial<CustomIconComponentProps & IconProps>) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
component={() => (
|
||||
<svg
|
||||
className="chatico"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M252.068571 906.496h520.283429c89.581714 0 134.144-44.562286 134.144-132.845714V250.331429c0-88.283429-44.562286-132.845714-134.144-132.845715H252.068571c-89.142857 0-134.582857 44.141714-134.582857 132.845715V773.668571c0 88.704 45.44 132.845714 134.582857 132.845715z m1.28-68.992c-42.843429 0-66.852571-22.710857-66.852571-67.291429V253.805714c0-44.580571 24.009143-67.291429 66.852571-67.291428h222.866286v651.008z m517.723429-651.008c42.422857 0 66.432 22.710857 66.432 67.291429V770.194286c0 44.580571-24.009143 67.291429-66.432 67.291428H548.205714V186.496z" />
|
||||
</svg>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/layout/index.scss
vendored
@@ -1,10 +1,20 @@
|
||||
.chat-logo {
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
height: 48px;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-info {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
||||
.ant-tag {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +23,21 @@
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.ant-layout-sider-children {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
.ant-menu-item {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.ant-layout-footer {
|
||||
color: #666 !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
149
src/layout/index.tsx
vendored
@@ -1,55 +1,118 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { Layout, Menu } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { getName, getVersion } from '@tauri-apps/api/app';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import Routes, { menuItems } from '@/routes';
|
||||
import './index.scss';
|
||||
|
||||
const { Content, Footer, Sider } = Layout;
|
||||
|
||||
interface ChatLayoutProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChatLayout: FC<ChatLayoutProps> = ({ children }) => {
|
||||
export default function ChatLayout() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [isDashboard, setDashboard] = useState<any>(null);
|
||||
const [appInfo, setAppInfo] = useState<Record<string, any>>({});
|
||||
const location = useLocation();
|
||||
const [menuKey, setMenuKey] = useState(location.pathname);
|
||||
const go = useNavigate();
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }} hasSider>
|
||||
<Sider
|
||||
theme="light"
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<div className="chat-logo"><img src="/logo.png" /></div>
|
||||
<Menu defaultSelectedKeys={[location.pathname]} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} />
|
||||
</Sider>
|
||||
<Layout className="chat-layout" style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}>
|
||||
<Content
|
||||
className="chat-container"
|
||||
style={{
|
||||
overflow: 'inherit'
|
||||
}}
|
||||
>
|
||||
<Routes />
|
||||
</Content>
|
||||
<Footer style={{ textAlign: 'center' }}>
|
||||
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (location.search === '?type=control') {
|
||||
go('/settings');
|
||||
}
|
||||
if (location.search === '?type=preview') {
|
||||
go('/?type=preview');
|
||||
}
|
||||
setMenuKey(location.pathname);
|
||||
setDashboard(location.pathname === '/');
|
||||
}, [location.pathname]);
|
||||
|
||||
export default ChatLayout;
|
||||
useInit(async () => {
|
||||
setAppInfo({
|
||||
appName: await getName(),
|
||||
appVersion: await getVersion(),
|
||||
appTheme: await invoke('get_theme'),
|
||||
});
|
||||
});
|
||||
|
||||
const checkAppUpdate = async () => {
|
||||
await invoke('run_check_update', { silent: false, hasMsg: true });
|
||||
};
|
||||
|
||||
const isDark = appInfo.appTheme === 'dark';
|
||||
|
||||
if (isDashboard === null) return null;
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}>
|
||||
{isDashboard ? (
|
||||
<Routes />
|
||||
) : (
|
||||
<Layout style={{ minHeight: '100vh' }} hasSider>
|
||||
<Sider
|
||||
theme={isDark ? 'dark' : 'light'}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<div className="chat-logo">
|
||||
<img src="/logo.png" />
|
||||
</div>
|
||||
<div className="chat-info">
|
||||
<Tag>{appInfo.appName}</Tag>
|
||||
<Tag>
|
||||
<span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
|
||||
<Tooltip title="click to check update">
|
||||
<a onClick={checkAppUpdate}>
|
||||
<SyncOutlined />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
selectedKeys={[menuKey]}
|
||||
mode="inline"
|
||||
theme={appInfo.appTheme === 'dark' ? 'dark' : 'light'}
|
||||
inlineIndent={12}
|
||||
items={menuItems}
|
||||
// defaultOpenKeys={['/model']}
|
||||
onClick={(i) => go(i.key)}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout
|
||||
className="chat-layout"
|
||||
style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}
|
||||
>
|
||||
<Content
|
||||
className="chat-container"
|
||||
style={{
|
||||
overflow: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Routes />
|
||||
</Content>
|
||||
<Footer style={{ textAlign: 'center' }}>
|
||||
<a href="https://github.com/lencx/chatgpt" target="_blank">
|
||||
ChatGPT Desktop Application
|
||||
</a>{' '}
|
||||
©2022 Created by lencx
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)}
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
95
src/main.scss
vendored
@@ -14,7 +14,98 @@
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-table-selection-col,
|
||||
.ant-table-selection-column {
|
||||
width: 50px !important;
|
||||
min-width: 50px !important;
|
||||
}
|
||||
|
||||
.chat-prompts-val {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.ellipsis-line {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rowedit {
|
||||
padding: 2px 5px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 2px rgba(237, 122, 60, 0.8);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-add-btn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chat-tags,
|
||||
.chat-prompts-tags {
|
||||
.ant-tag {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-table-tip {
|
||||
> span {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-file-path {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
margin-bottom: 3px;
|
||||
line-height: 16px;
|
||||
|
||||
> div {
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
// background-color: #d8d8d8;
|
||||
color: #4096ff;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.chatico {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.awesome-tips {
|
||||
.ant-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
4
src/main.tsx
vendored
@@ -9,8 +9,8 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<Suspense fallback={null}>
|
||||
<BrowserRouter>
|
||||
<Layout/>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
137
src/routes.tsx
vendored
@@ -1,54 +1,145 @@
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import {
|
||||
DesktopOutlined,
|
||||
SettingOutlined,
|
||||
BulbOutlined,
|
||||
SyncOutlined,
|
||||
FileSyncOutlined,
|
||||
UserOutlined,
|
||||
DownloadOutlined,
|
||||
FormOutlined,
|
||||
GlobalOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import type { MenuProps } from 'antd';
|
||||
|
||||
import General from '@view/General';
|
||||
import LanguageModel from '@/view/LanguageModel';
|
||||
import SyncPrompts from '@/view/SyncPrompts';
|
||||
import Settings from '@/view/settings';
|
||||
import About from '@/view/about';
|
||||
import Awesome from '@/view/awesome';
|
||||
import UserCustom from '@/view/model/UserCustom';
|
||||
import SyncPrompts from '@/view/model/SyncPrompts';
|
||||
import SyncCustom from '@/view/model/SyncCustom';
|
||||
import SyncRecord from '@/view/model/SyncRecord';
|
||||
import Download from '@/view/download';
|
||||
import Notes from '@/view/notes';
|
||||
import Markdown from '@/view/markdown';
|
||||
import Dashboard from '@/view/dashboard';
|
||||
|
||||
export type ChatRouteObject = {
|
||||
export type ChatRouteMetaObject = {
|
||||
label: string;
|
||||
icon?: React.ReactNode,
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const routes: Array<RouteObject & { meta: ChatRouteObject }> = [
|
||||
type ChatRouteObject = {
|
||||
path: string;
|
||||
element?: JSX.Element;
|
||||
hideMenu?: boolean;
|
||||
meta?: ChatRouteMetaObject;
|
||||
children?: ChatRouteObject[];
|
||||
};
|
||||
|
||||
export const routes: Array<ChatRouteObject> = [
|
||||
{
|
||||
path: '/',
|
||||
element: <General />,
|
||||
path: '/settings',
|
||||
element: <Settings />,
|
||||
meta: {
|
||||
label: 'General',
|
||||
icon: <DesktopOutlined />,
|
||||
label: 'Settings',
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/language-model',
|
||||
element: <LanguageModel />,
|
||||
path: '/awesome',
|
||||
element: <Awesome />,
|
||||
meta: {
|
||||
label: 'Awesome',
|
||||
icon: <GlobalOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/notes',
|
||||
element: <Notes />,
|
||||
meta: {
|
||||
label: 'Notes',
|
||||
icon: <FormOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/md/:id',
|
||||
element: <Markdown />,
|
||||
hideMenu: true,
|
||||
},
|
||||
{
|
||||
path: '/model',
|
||||
meta: {
|
||||
label: 'Language Model',
|
||||
icon: <BulbOutlined />,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user-custom',
|
||||
element: <UserCustom />,
|
||||
meta: {
|
||||
label: 'User Custom',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
},
|
||||
// --- Sync
|
||||
{
|
||||
path: 'sync-prompts',
|
||||
element: <SyncPrompts />,
|
||||
meta: {
|
||||
label: 'Sync Prompts',
|
||||
icon: <SyncOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'sync-custom',
|
||||
element: <SyncCustom />,
|
||||
meta: {
|
||||
label: 'Sync Custom',
|
||||
icon: <FileSyncOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'sync-custom/:id',
|
||||
element: <SyncRecord />,
|
||||
hideMenu: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/sync-prompts',
|
||||
element: <SyncPrompts />,
|
||||
path: '/download',
|
||||
element: <Download />,
|
||||
meta: {
|
||||
label: 'Sync Prompts',
|
||||
icon: <SyncOutlined />,
|
||||
label: 'Download',
|
||||
icon: <DownloadOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: <About />,
|
||||
meta: {
|
||||
label: 'About',
|
||||
icon: <InfoCircleOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <Dashboard />,
|
||||
hideMenu: true,
|
||||
},
|
||||
];
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
export const menuItems: MenuItem[] = routes.map(i => ({
|
||||
...i.meta,
|
||||
key: i.path || '',
|
||||
}));
|
||||
export const menuItems: MenuItem[] = routes
|
||||
.filter((j) => !j.hideMenu)
|
||||
.map((i) => ({
|
||||
...i.meta,
|
||||
key: i.path || '',
|
||||
children: i?.children
|
||||
?.filter((j) => !j.hideMenu)
|
||||
?.map((j) => ({ ...j.meta, key: `${i.path}/${j.path}` || '' })),
|
||||
}));
|
||||
|
||||
export default () => {
|
||||
return useRoutes(routes);
|
||||
};
|
||||
};
|
||||
|
||||
93
src/utils.ts
vendored
@@ -1,51 +1,98 @@
|
||||
import { readTextFile, writeTextFile, exists } from '@tauri-apps/api/fs';
|
||||
import { homeDir, join } from '@tauri-apps/api/path';
|
||||
import { readTextFile, writeTextFile, exists, createDir } from '@tauri-apps/api/fs';
|
||||
import { homeDir, join, dirname } from '@tauri-apps/api/path';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const CHAT_CONF_JSON = 'chat.conf.json';
|
||||
export const CHAT_MODEL_JSON = 'chat.model.json';
|
||||
export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json';
|
||||
export const CHAT_DOWNLOAD_JSON = 'chat.download.json';
|
||||
export const CHAT_AWESOME_JSON = 'chat.awesome.json';
|
||||
export const CHAT_NOTES_JSON = 'chat.notes.json';
|
||||
export const CHAT_PROMPTS_CSV = 'chat.prompts.csv';
|
||||
export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
|
||||
export const GITHUB_PROMPTS_CSV_URL =
|
||||
'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
|
||||
export const GITHUB_LOG_URL = 'https://raw.githubusercontent.com/lencx/ChatGPT/main/UPDATE_LOG.md';
|
||||
|
||||
export const DISABLE_AUTO_COMPLETE = {
|
||||
autoCapitalize: 'off',
|
||||
autoComplete: 'off',
|
||||
spellCheck: false
|
||||
spellCheck: false,
|
||||
};
|
||||
|
||||
export const chatRoot = async () => {
|
||||
return join(await homeDir(), '.chatgpt')
|
||||
}
|
||||
return join(await homeDir(), '.chatgpt');
|
||||
};
|
||||
|
||||
export const chatModelPath = async (): Promise<string> => {
|
||||
return join(await chatRoot(), CHAT_MODEL_JSON);
|
||||
}
|
||||
};
|
||||
|
||||
export const chatPromptsPath = async (): Promise<string> => {
|
||||
return join(await chatRoot(), CHAT_PROMPTS_CSV);
|
||||
}
|
||||
};
|
||||
|
||||
export const readJSON = async (path: string, defaultVal = {}) => {
|
||||
type readJSONOpts = { defaultVal?: Record<string, any>; isRoot?: boolean; isList?: boolean };
|
||||
export const readJSON = async (path: string, opts: readJSONOpts = {}) => {
|
||||
const { defaultVal = {}, isRoot = false, isList = false } = opts;
|
||||
const root = await chatRoot();
|
||||
const file = await join(root, path);
|
||||
let file = path;
|
||||
|
||||
if (!await exists(file)) {
|
||||
writeTextFile(file, JSON.stringify({
|
||||
name: 'ChatGPT',
|
||||
link: 'https://github.com/lencx/ChatGPT/blob/main/chat.model.md',
|
||||
...defaultVal,
|
||||
}, null, 2))
|
||||
if (!isRoot) {
|
||||
file = await join(root, path);
|
||||
}
|
||||
|
||||
if (!(await exists(file))) {
|
||||
if ((await dirname(file)) !== root) {
|
||||
await createDir(await dirname(file), { recursive: true });
|
||||
}
|
||||
await writeTextFile(
|
||||
file,
|
||||
isList
|
||||
? '[]'
|
||||
: JSON.stringify(
|
||||
{
|
||||
name: 'ChatGPT',
|
||||
link: 'https://github.com/lencx/ChatGPT',
|
||||
...defaultVal,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(await readTextFile(file));
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const writeJSON = async (path: string, data: Record<string, any>) => {
|
||||
type writeJSONOpts = { dir?: string; isRoot?: boolean };
|
||||
export const writeJSON = async (
|
||||
path: string,
|
||||
data: Record<string, any>,
|
||||
opts: writeJSONOpts = {},
|
||||
) => {
|
||||
const { isRoot = false } = opts;
|
||||
const root = await chatRoot();
|
||||
const file = await join(root, path);
|
||||
await writeTextFile(file, JSON.stringify(data, null, 2));
|
||||
}
|
||||
let file = path;
|
||||
|
||||
export const fmtDate = (date: any) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (!isRoot) {
|
||||
file = await join(root, path);
|
||||
}
|
||||
|
||||
if (isRoot && !(await exists(await dirname(file)))) {
|
||||
await createDir(await dirname(file), { recursive: true });
|
||||
}
|
||||
|
||||
await writeTextFile(file, JSON.stringify(data, null, 2));
|
||||
};
|
||||
|
||||
export const fmtDate = (date: any) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
export const genCmd = (act: string) =>
|
||||
act
|
||||
.replace(/\s+|\/+/g, '_')
|
||||
.replace(/[^\d\w]/g, '')
|
||||
.toLocaleLowerCase();
|
||||
|
||||
98
src/view/General.tsx
vendored
@@ -1,98 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Radio, Switch, Input, Button, Space, message, Tooltip } from 'antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { platform } from '@tauri-apps/api/os';
|
||||
import { ask } from '@tauri-apps/api/dialog';
|
||||
import { relaunch } from '@tauri-apps/api/process';
|
||||
import { clone, omit, isEqual } from 'lodash';
|
||||
|
||||
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||
|
||||
const OriginLabel = ({ url }: { url: string }) => {
|
||||
return (
|
||||
<span>
|
||||
Switch Origin <Tooltip title={`Default: ${url}`}><QuestionCircleOutlined /></Tooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function General() {
|
||||
const [form] = Form.useForm();
|
||||
const [platformInfo, setPlatform] = useState<string>('');
|
||||
const [chatConf, setChatConf] = useState<any>(null);
|
||||
|
||||
const init = async () => {
|
||||
setPlatform(await platform());
|
||||
const chatData = await invoke('get_chat_conf');
|
||||
setChatConf(chatData);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(clone(chatConf));
|
||||
}, [chatConf])
|
||||
|
||||
const onCancel = () => {
|
||||
form.setFieldsValue(chatConf);
|
||||
};
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
if (!isEqual(omit(chatConf, ['default_origin']), values)) {
|
||||
await invoke('form_confirm', { data: values, label: 'main' });
|
||||
const isOk = await ask(`Configuration saved successfully, whether to restart?`, {
|
||||
title: 'ChatGPT Preferences'
|
||||
});
|
||||
if (isOk) {
|
||||
relaunch();
|
||||
return;
|
||||
}
|
||||
message.success('Configuration saved successfully');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
style={{ maxWidth: 500 }}
|
||||
onFinish={onFinish}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 15, offset: 1 }}
|
||||
>
|
||||
<Form.Item label="Theme" name="theme">
|
||||
<Radio.Group>
|
||||
<Radio value="Light">Light</Radio>
|
||||
<Radio value="Dark">Dark</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{platformInfo === 'darwin' && (
|
||||
<Form.Item label="Titlebar" name="titlebar" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={<OriginLabel url={chatConf?.default_origin} />} name="origin">
|
||||
<Input placeholder="https://chat.openai.com" {...DISABLE_AUTO_COMPLETE} />
|
||||
</Form.Item>
|
||||
<Form.Item label="User Agent (Window)" name="ua_window">
|
||||
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
|
||||
</Form.Item>
|
||||
<Form.Item label="User Agent (SystemTray)" name="ua_tray">
|
||||
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space size={20}>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
55
src/view/LanguageModel/config.tsx
vendored
@@ -1,55 +0,0 @@
|
||||
import { Tag, Switch, Tooltip, Space } from 'antd';
|
||||
|
||||
export const modelColumns = () => [
|
||||
{
|
||||
title: '/{cmd}',
|
||||
dataIndex: 'cmd',
|
||||
fixed: 'left',
|
||||
width: 120,
|
||||
key: 'cmd',
|
||||
render: (v: string) => <Tag color="#2a2a2a">/{v}</Tag>
|
||||
},
|
||||
{
|
||||
title: 'Act',
|
||||
dataIndex: 'act',
|
||||
key: 'act',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
width: 150,
|
||||
render: (v: string[]) => (
|
||||
<span className="chat-prompts-tags">{v?.map(i => <Tag key={i}>{i}</Tag>)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Enable',
|
||||
dataIndex: 'enable',
|
||||
key: 'enable',
|
||||
width: 80,
|
||||
render: (v: boolean = false) => <Switch checked={v} disabled />,
|
||||
},
|
||||
{
|
||||
title: 'Prompt',
|
||||
dataIndex: 'prompt',
|
||||
key: 'prompt',
|
||||
width: 300,
|
||||
render: (v: string) => (
|
||||
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
render: (_: any, row: any, actions: any) => (
|
||||
<Space size="middle">
|
||||
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||
<a onClick={() => actions.setRecord(row, 'delete')}>Delete</a>
|
||||
</Space>
|
||||
),
|
||||
}
|
||||
];
|
||||
39
src/view/LanguageModel/index.scss
vendored
@@ -1,39 +0,0 @@
|
||||
.chat-prompts-val {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.chat-prompts-tags {
|
||||
.ant-tag {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chat-model-path {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #888;
|
||||
margin-bottom: 5px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
// background-color: #d8d8d8;
|
||||
color: #4096ff;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
99
src/view/LanguageModel/index.tsx
vendored
@@ -1,99 +0,0 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Table, Button, Modal, message } from 'antd';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import useChatModel from '@/hooks/useChatModel';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import useData from '@/hooks/useData';
|
||||
import { chatModelPath } from '@/utils';
|
||||
import { modelColumns } from './config';
|
||||
import LanguageModelForm from './Form';
|
||||
import './index.scss';
|
||||
|
||||
export default function LanguageModel() {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [modelPath, setChatModelPath] = useState('');
|
||||
const { modelData, modelSet } = useChatModel('user_custom');
|
||||
const { opData, opAdd, opRemove, opReplace, opSafeKey } = useData(modelData);
|
||||
const { columns, ...opInfo } = useColumns(modelColumns());
|
||||
const formRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opInfo.opType) return;
|
||||
if (['edit', 'new'].includes(opInfo.opType)) {
|
||||
setVisible(true);
|
||||
}
|
||||
if (['delete'].includes(opInfo.opType)) {
|
||||
const data = opRemove(opInfo?.opRecord?.[opSafeKey]);
|
||||
modelSet(data);
|
||||
opInfo.resetRecord();
|
||||
}
|
||||
}, [opInfo.opType, formRef]);
|
||||
|
||||
useInit(async () => {
|
||||
const path = await chatModelPath();
|
||||
setChatModelPath(path);
|
||||
})
|
||||
|
||||
const hide = () => {
|
||||
setVisible(false);
|
||||
opInfo.resetRecord();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
formRef.current?.form?.validateFields()
|
||||
.then((vals: Record<string, any>) => {
|
||||
if (modelData.map((i: any) => i.cmd).includes(vals.cmd) && opInfo?.opRecord?.cmd !== vals.cmd) {
|
||||
message.warning(`"cmd: /${vals.cmd}" already exists, please change the "${vals.cmd}" name and resubmit.`);
|
||||
return;
|
||||
}
|
||||
let data = [];
|
||||
switch (opInfo.opType) {
|
||||
case 'new': data = opAdd(vals); break;
|
||||
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break;
|
||||
default: break;
|
||||
}
|
||||
modelSet(data)
|
||||
hide();
|
||||
})
|
||||
};
|
||||
|
||||
const handleOpenFile = () => {
|
||||
invoke('open_file', { path: modelPath });
|
||||
};
|
||||
|
||||
const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Language Model`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button className="add-btn" type="primary" onClick={opInfo.opNew}>Add Model</Button>
|
||||
<div className="chat-model-path">PATH: <span onClick={handleOpenFile}>{modelPath}</span></div>
|
||||
<Table
|
||||
key={opInfo.opTime}
|
||||
rowKey="cmd"
|
||||
columns={columns}
|
||||
scroll={{ x: 'auto' }}
|
||||
dataSource={opData}
|
||||
pagination={{
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 5,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
showTotal: (total) => <span>Total {total} items</span>,
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
onCancel={hide}
|
||||
title={modalTitle}
|
||||
onOk={handleOk}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<LanguageModelForm record={opInfo?.opRecord} ref={formRef} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
src/view/SyncPrompts/index.scss
vendored
@@ -1,28 +0,0 @@
|
||||
.chat-prompts-tags {
|
||||
.ant-tag {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chat-model-path {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #888;
|
||||
margin-bottom: 5px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
// background-color: #d8d8d8;
|
||||
color: #4096ff;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
71
src/view/SyncPrompts/index.tsx
vendored
@@ -1,71 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Table, Button, message } from 'antd';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { fetch, ResponseType } from '@tauri-apps/api/http';
|
||||
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs';
|
||||
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import useChatModel from '@/hooks/useChatModel';
|
||||
import { fmtDate, chatPromptsPath, GITHUB_PROMPTS_CSV_URL } from '@/utils';
|
||||
import { modelColumns, genCmd } from './config';
|
||||
import './index.scss';
|
||||
import useInit from '@/hooks/useInit';
|
||||
|
||||
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
|
||||
|
||||
export default function LanguageModel() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lastUpdated, setLastUpdated] = useState();
|
||||
const { modelSet } = useChatModel('sys_sync_prompts');
|
||||
const [tableData, setTableData] = useState<Record<string, string>[]>([]);
|
||||
const { columns, ...opInfo } = useColumns(modelColumns());
|
||||
|
||||
useInit(async () => {
|
||||
const filename = await chatPromptsPath();
|
||||
const data = await readTextFile(filename);
|
||||
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
|
||||
const fileData: Record<string, any> = await invoke('metadata', { path: filename });
|
||||
setLastUpdated(fileData.accessedAtMs);
|
||||
setTableData(list);
|
||||
})
|
||||
|
||||
const handleSync = async () => {
|
||||
setLoading(true);
|
||||
const res = await fetch(GITHUB_PROMPTS_CSV_URL, {
|
||||
method: 'GET',
|
||||
responseType: ResponseType.Text,
|
||||
});
|
||||
const data = (res.data || '') as string;
|
||||
// const content = data.replace(/"(\s+)?,(\s+)?"/g, '","');
|
||||
await writeTextFile(await chatPromptsPath(), data);
|
||||
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
|
||||
setTableData(list);
|
||||
modelSet(list.map(i => ({ cmd: genCmd(i.act), enable: true, tags: ['chatgpt-prompts'], ...i })));
|
||||
setLoading(false);
|
||||
setLastUpdated(fmtDate(Date.now()) as any);
|
||||
message.success('ChatGPT Prompts data synchronization completed!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button type="primary" loading={loading} onClick={handleSync}>Sync</Button>
|
||||
{lastUpdated && <span style={{ marginLeft: 10, color: '#999' }}>Last updated on {fmtDate(lastUpdated)}</span>}
|
||||
<div className="chat-model-path">URL: <a href={promptsURL} target="_blank">{promptsURL}</a></div>
|
||||
<Table
|
||||
key={lastUpdated}
|
||||
rowKey="act"
|
||||
columns={columns}
|
||||
scroll={{ x: 'auto' }}
|
||||
dataSource={tableData}
|
||||
pagination={{
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 5,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
showTotal: (total) => <span>Total {total} items</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
src/view/about/index.scss
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
.about {
|
||||
.log-tab {
|
||||
font-size: 14px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.about-tab {
|
||||
.imgs {
|
||||
img {
|
||||
max-width: 200px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(200, 200, 200, 0.4);
|
||||
padding: 2px 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
88
src/view/about/index.tsx
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { Tabs, Tag } from 'antd';
|
||||
|
||||
import { GITHUB_LOG_URL } from '@/utils';
|
||||
import useInit from '@/hooks/useInit';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import './index.scss';
|
||||
|
||||
export default function About() {
|
||||
const [logContent, setLogContent] = useState('');
|
||||
|
||||
useInit(async () => {
|
||||
const data = (await invoke('get_data', { url: GITHUB_LOG_URL })) || '';
|
||||
setLogContent(data as string);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="about">
|
||||
<Tabs
|
||||
items={[
|
||||
{ label: 'About ChatGPT', key: 'about', children: <AboutChatGPT /> },
|
||||
{ label: 'Update Log', key: 'log', children: <LogTab content={logContent} /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AboutChatGPT = () => {
|
||||
return (
|
||||
<div className="about-tab">
|
||||
<Tag>ChatGPT Desktop Application (Mac, Windows and Linux)</Tag>
|
||||
<p>
|
||||
🕒 History versions:{' '}
|
||||
<a href="https://github.com/lencx/ChatGPT/releases" target="_blank">
|
||||
lencx/ChatGPT/releases
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
It is just a wrapper for the
|
||||
<a href="https://chat.openai.com" target="_blank" title="https://chat.openai.com">
|
||||
{' '}
|
||||
OpenAI ChatGPT{' '}
|
||||
</a>
|
||||
website, no other data transfer exists (you can check the{' '}
|
||||
<a
|
||||
href="https://github.com/lencx/ChatGPT"
|
||||
target="_blank"
|
||||
title="https://github.com/lencx/ChatGPT"
|
||||
>
|
||||
{' '}
|
||||
source code{' '}
|
||||
</a>
|
||||
). The development and maintenance of this software has taken up a lot of my time. If it
|
||||
helps you, you can buy me a cup of coffee (Chinese users can use WeChat to scan the code),
|
||||
thanks!
|
||||
</p>
|
||||
<p className="imgs">
|
||||
<a href="https://www.buymeacoffee.com/lencx" target="_blank">
|
||||
<img
|
||||
src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png"
|
||||
alt="Buy Me A Coffee"
|
||||
/>
|
||||
</a>{' '}
|
||||
<br />
|
||||
<img
|
||||
width="200"
|
||||
src="https://user-images.githubusercontent.com/16164244/207228025-117b5f77-c5d2-48c2-a070-774b7a1596f2.png"
|
||||
></img>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LogTab = ({ content }: { content: string }) => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Ref:{' '}
|
||||
<a href="https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md" target="_blank">
|
||||
lencx/ChatGPT/UPDATE_LOG.md
|
||||
</a>
|
||||
</p>
|
||||
<Markdown className="log-tab" children={content} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
63
src/view/awesome/Form.tsx
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react';
|
||||
import { Form, Input, Switch } from 'antd';
|
||||
import type { FormProps } from 'antd';
|
||||
|
||||
import Tags from '@comps/Tags';
|
||||
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||
|
||||
interface AwesomeFormProps {
|
||||
record?: Record<string | symbol, any> | null;
|
||||
}
|
||||
|
||||
const initFormValue = {
|
||||
title: '',
|
||||
url: '',
|
||||
enable: true,
|
||||
tags: [],
|
||||
category: '',
|
||||
};
|
||||
|
||||
const AwesomeForm: ForwardRefRenderFunction<FormProps, AwesomeFormProps> = ({ record }, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
useImperativeHandle(ref, () => ({ form }));
|
||||
|
||||
useEffect(() => {
|
||||
if (record) {
|
||||
form.setFieldsValue(record);
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
return (
|
||||
<Form form={form} labelCol={{ span: 4 }} initialValues={initFormValue}>
|
||||
<Form.Item
|
||||
label="Title"
|
||||
name="title"
|
||||
rules={[{ required: true, message: 'Please enter a title!' }]}
|
||||
>
|
||||
<Input placeholder="Please enter a title" {...DISABLE_AUTO_COMPLETE} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="URL"
|
||||
name="url"
|
||||
rules={[{ required: true, message: 'Please enter the URL' }, { type: 'url' }]}
|
||||
>
|
||||
<Input placeholder="Please enter the URL" {...DISABLE_AUTO_COMPLETE} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Category"
|
||||
name="category"
|
||||
rules={[{ required: true, message: 'Please enter a category' }]}
|
||||
>
|
||||
<Input placeholder="Please enter a category" {...DISABLE_AUTO_COMPLETE} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Tags" name="tags">
|
||||
<Tags value={record?.tags} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Enable" name="enable" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(AwesomeForm);
|
||||
74
src/view/awesome/config.tsx
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Tag, Space, Popconfirm, Switch } from 'antd';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
export const awesomeColumns = () => [
|
||||
{
|
||||
title: 'Title',
|
||||
dataIndex: 'title',
|
||||
fixed: 'left',
|
||||
key: 'title',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
width: 200,
|
||||
render: (v: string) => <a onClick={() => open(v)}>{v}</a>,
|
||||
},
|
||||
// {
|
||||
// title: 'Icon',
|
||||
// dataIndex: 'icon',
|
||||
// key: 'icon',
|
||||
// width: 120,
|
||||
// },
|
||||
{
|
||||
title: 'Enable',
|
||||
dataIndex: 'enable',
|
||||
key: 'enable',
|
||||
width: 80,
|
||||
render: (v: boolean = true, row: Record<string, any>, action: Record<string, any>) => (
|
||||
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (v: string) => <Tag color="geekblue">{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
width: 150,
|
||||
render: (v: string[]) => (
|
||||
<span className="chat-tags">
|
||||
{v?.map((i) => (
|
||||
<Tag key={i}>{i}</Tag>
|
||||
))}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
render: (_: any, row: any, actions: any) => {
|
||||
return (
|
||||
<Space>
|
||||
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||
<Popconfirm
|
||||
title="Are you sure you want to delete this URL?"
|
||||
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<a>Delete</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
197
src/view/awesome/index.tsx
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Table, Modal, Popconfirm, Button, Tooltip, Tag, message } from 'antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
import useJson from '@/hooks/useJson';
|
||||
import useData from '@/hooks/useData';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import FilePath from '@/components/FilePath';
|
||||
import { CHAT_AWESOME_JSON } from '@/utils';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { awesomeColumns } from './config';
|
||||
import AwesomeForm from './Form';
|
||||
|
||||
export default function Awesome() {
|
||||
const formRef = useRef<any>(null);
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { opData, opInit, opAdd, opReplace, opReplaceItems, opRemove, opRemoveItems, opSafeKey } =
|
||||
useData([]);
|
||||
const { columns, ...opInfo } = useColumns(awesomeColumns());
|
||||
const { rowSelection, selectedRowIDs, rowReset } = useTableRowSelection();
|
||||
const { json, updateJson } = useJson<any[]>(CHAT_AWESOME_JSON);
|
||||
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!json || json.length <= 0) return;
|
||||
opInit(json);
|
||||
}, [json?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opInfo.opType) return;
|
||||
if (['edit', 'new'].includes(opInfo.opType)) {
|
||||
setVisible(true);
|
||||
}
|
||||
if (['delete'].includes(opInfo.opType)) {
|
||||
const data = opRemove(opInfo?.opRecord?.[opSafeKey]);
|
||||
updateJson(data);
|
||||
opInfo.resetRecord();
|
||||
}
|
||||
}, [opInfo.opType, formRef]);
|
||||
|
||||
const hide = () => {
|
||||
setVisible(false);
|
||||
opInfo.resetRecord();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (opInfo.opType === 'enable') {
|
||||
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||
updateJson(data);
|
||||
}
|
||||
}, [opInfo.opTime]);
|
||||
|
||||
const handleDelete = () => {
|
||||
const data = opRemoveItems(selectedRowIDs);
|
||||
updateJson(data);
|
||||
rowReset();
|
||||
message.success('All selected URLs have been deleted');
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
formRef.current?.form?.validateFields().then(async (vals: Record<string, any>) => {
|
||||
let idx = opData.findIndex((i) => i.url === vals.url);
|
||||
if (vals.url === opInfo?.opRecord?.url) {
|
||||
idx = -1;
|
||||
}
|
||||
if (idx === -1) {
|
||||
if (opInfo.opType === 'new') {
|
||||
const data = opAdd(vals);
|
||||
await updateJson(data);
|
||||
opInit(data);
|
||||
message.success('Data added successfully');
|
||||
}
|
||||
if (opInfo.opType === 'edit') {
|
||||
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
|
||||
await updateJson(data);
|
||||
message.success('Data updated successfully');
|
||||
}
|
||||
hide();
|
||||
} else {
|
||||
const data = opData[idx];
|
||||
message.error(
|
||||
<div style={{ width: 360 }}>
|
||||
<div>
|
||||
<b>
|
||||
{data.title}: {data.url}
|
||||
</b>
|
||||
</div>
|
||||
<div>This URL already exists, please edit it and try again.</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnable = (isEnable: boolean) => {
|
||||
const data = opReplaceItems(selectedRowIDs, { enable: isEnable });
|
||||
updateJson(data);
|
||||
};
|
||||
|
||||
const handlePreview = () => {
|
||||
invoke('wa_window', {
|
||||
label: 'awesome_preview',
|
||||
url: 'index.html?type=preview',
|
||||
title: 'Preview Dashboard',
|
||||
});
|
||||
};
|
||||
|
||||
const modalTitle = `${{ new: 'Create', edit: 'Edit' }[opInfo.opType]} URL`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-table-btns">
|
||||
<div>
|
||||
<Button className="chat-add-btn" type="primary" onClick={opInfo.opNew}>
|
||||
Add URL
|
||||
</Button>
|
||||
<Button type="dashed" onClick={handlePreview}>
|
||||
Preview Dashboard
|
||||
</Button>
|
||||
<PreviewTip />
|
||||
</div>
|
||||
<div>
|
||||
{selectedItems.length > 0 && (
|
||||
<>
|
||||
<Button type="primary" onClick={() => handleEnable(true)}>
|
||||
Enable
|
||||
</Button>
|
||||
<Button onClick={() => handleEnable(false)}>Disable</Button>
|
||||
<Popconfirm
|
||||
overlayStyle={{ width: 250 }}
|
||||
title="URLs cannot be recovered after deletion, are you sure you want to delete them?"
|
||||
placement="topLeft"
|
||||
onConfirm={handleDelete}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button>Delete</Button>
|
||||
</Popconfirm>
|
||||
<span className="num">Selected {selectedItems.length} items</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FilePath paths={CHAT_AWESOME_JSON} />
|
||||
<Table
|
||||
rowKey="url"
|
||||
columns={columns}
|
||||
scroll={{ x: 800 }}
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
title={modalTitle}
|
||||
onCancel={hide}
|
||||
onOk={handleOk}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
>
|
||||
<AwesomeForm ref={formRef} record={opInfo?.opRecord} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PreviewTip = () => {
|
||||
const go = useNavigate();
|
||||
const handleGo = (v: string) => {
|
||||
go(`/settings?type=${v}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
overlayInnerStyle={{ width: 400 }}
|
||||
title={
|
||||
<div className="awesome-tips">
|
||||
Click the button to preview, and in
|
||||
<Link to="/settings"> Settings </Link>
|
||||
you can set a single URL or Dashboard as the default window for the app.
|
||||
<br />
|
||||
<Tag onClick={() => handleGo('main_window')} color="blue">
|
||||
Main Window
|
||||
</Tag>
|
||||
{'or '}
|
||||
<Tag onClick={() => handleGo('tray_window')} color="blue">
|
||||
SystemTray Window
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ marginLeft: 5, color: '#1677ff' }} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
73
src/view/dashboard/index.scss
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
.dashboard {
|
||||
position: fixed;
|
||||
width: calc(100% - 30px);
|
||||
height: calc(100% - 30px);
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
|
||||
&-no-data {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
color: #989898;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.txt {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
|
||||
a {
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
&.has-top-dom {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
&.preview {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.group-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
.ant-card-body {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/view/dashboard/index.tsx
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Row, Col, Card } from 'antd';
|
||||
import { InboxOutlined } from '@ant-design/icons';
|
||||
import { os, invoke } from '@tauri-apps/api';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import useJson from '@/hooks/useJson';
|
||||
import { CHAT_AWESOME_JSON, CHAT_CONF_JSON, readJSON } from '@/utils';
|
||||
import './index.scss';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [params] = useSearchParams();
|
||||
const { json } = useJson<Record<string, any>[]>(CHAT_AWESOME_JSON);
|
||||
const [list, setList] = useState<Array<[string, Record<string, any>[]]>>();
|
||||
const [hasClass, setClass] = useState(false);
|
||||
const [theme, setTheme] = useState('');
|
||||
|
||||
useInit(async () => {
|
||||
const getOS = await os.platform();
|
||||
const conf = await readJSON(CHAT_CONF_JSON);
|
||||
const appTheme = await invoke('get_theme');
|
||||
setTheme(appTheme as string);
|
||||
setClass(!conf?.titlebar && getOS === 'darwin');
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!json) return;
|
||||
const categories = new Map();
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
json?.forEach((i) => {
|
||||
if (!i.enable) return;
|
||||
if (!categories.has(i.category)) {
|
||||
categories.set(i.category, []);
|
||||
}
|
||||
categories.get(i?.category).push(i);
|
||||
});
|
||||
setList(Array.from(categories) || []);
|
||||
} else {
|
||||
setList([]);
|
||||
}
|
||||
}, [JSON.stringify(json)]);
|
||||
|
||||
const handleLink = async (item: Record<string, any>) => {
|
||||
await invoke('wa_window', {
|
||||
label: btoa(item.url).replace(/[^a-zA-Z0-9]/g, ''),
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
});
|
||||
};
|
||||
|
||||
if (!list) return null;
|
||||
if (list?.length === 0) {
|
||||
return (
|
||||
<div className="dashboard-no-data">
|
||||
<div className="icon">
|
||||
<InboxOutlined style={{ fontSize: 80, marginBottom: 5 }} />
|
||||
<br />
|
||||
No data
|
||||
</div>
|
||||
<div className="txt">
|
||||
Go to <a onClick={() => invoke('control_window')}>{'Control Center -> Awesome'}</a> to add
|
||||
data and make sure they are enabled.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('dashboard', theme, {
|
||||
'has-top-dom': hasClass,
|
||||
preview: params.get('type') === 'preview',
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
{list.map((i) => {
|
||||
return (
|
||||
<div key={i[0]} className="group-item">
|
||||
<Card title={i[0]} size="small">
|
||||
<Row className="list" gutter={[8, 8]}>
|
||||
{i[1].map((j, idx) => {
|
||||
return (
|
||||
<Col
|
||||
title={`${j?.title}: ${j?.url}`}
|
||||
key={`${idx}_${j?.url}`}
|
||||
xl={4}
|
||||
md={6}
|
||||
sm={8}
|
||||
xs={12}
|
||||
>
|
||||
<Card className="item" hoverable onClick={() => handleLink(j)}>
|
||||
<span>{j?.title}</span>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/view/download/config.tsx
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { Tag, Space, Popconfirm } from 'antd';
|
||||
import { path, shell } from '@tauri-apps/api';
|
||||
|
||||
import { EditRow } from '@/hooks/useColumns';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import { fmtDate, chatRoot } from '@/utils';
|
||||
|
||||
const colorMap: any = {
|
||||
pdf: 'blue',
|
||||
png: 'orange',
|
||||
};
|
||||
|
||||
export const downloadColumns = () => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
fixed: 'left',
|
||||
key: 'name',
|
||||
width: 240,
|
||||
render: (_: string, row: any, actions: any) => (
|
||||
<EditRow rowKey="name" row={row} actions={actions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Extension',
|
||||
dataIndex: 'ext',
|
||||
key: 'ext',
|
||||
width: 120,
|
||||
render: (v: string) => <Tag color={colorMap[v]}>{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Path',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
width: 200,
|
||||
render: (_: string, row: any) => <RenderPath row={row} />,
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
width: 150,
|
||||
render: fmtDate,
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
render: (_: any, row: any, actions: any) => {
|
||||
return (
|
||||
<Space>
|
||||
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
|
||||
<Popconfirm
|
||||
title="Are you sure to delete this file?"
|
||||
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<a>Delete</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const RenderPath = ({ row }: any) => {
|
||||
const [filePath, setFilePath] = useState('');
|
||||
useInit(async () => {
|
||||
setFilePath(await getPath(row));
|
||||
});
|
||||
return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
|
||||
};
|
||||
|
||||
export const getPath = async (row: any) => {
|
||||
const isImg = ['png'].includes(row?.ext);
|
||||
return (
|
||||
(await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id)) + `.${row.ext}`
|
||||
);
|
||||
};
|
||||
145
src/view/download/index.tsx
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Modal, Popconfirm, Button, message } from 'antd';
|
||||
import { invoke, path, fs } from '@tauri-apps/api';
|
||||
|
||||
import useJson from '@/hooks/useJson';
|
||||
import useData from '@/hooks/useData';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import FilePath from '@/components/FilePath';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils';
|
||||
import { downloadColumns } from './config';
|
||||
|
||||
function renderFile(buff: Uint8Array, type: string) {
|
||||
const renderType = {
|
||||
pdf: 'application/pdf',
|
||||
png: 'image/png',
|
||||
}[type];
|
||||
return URL.createObjectURL(new Blob([buff], { type: renderType }));
|
||||
}
|
||||
|
||||
export default function Download() {
|
||||
const [source, setSource] = useState('');
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { opData, opInit, opReplace, opSafeKey } = useData([]);
|
||||
const { columns, ...opInfo } = useColumns(downloadColumns());
|
||||
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
|
||||
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
|
||||
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!json || json.length <= 0) return;
|
||||
opInit(json);
|
||||
}, [json?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opInfo.opType) return;
|
||||
(async () => {
|
||||
const record = opInfo?.opRecord;
|
||||
const isImg = ['png'].includes(record?.ext);
|
||||
const file = await path.join(
|
||||
await chatRoot(),
|
||||
'download',
|
||||
isImg ? 'img' : record?.ext,
|
||||
`${record?.id}.${record?.ext}`,
|
||||
);
|
||||
if (opInfo.opType === 'preview') {
|
||||
const data = await fs.readBinaryFile(file);
|
||||
const sourceData = renderFile(data, record?.ext);
|
||||
setSource(sourceData);
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
if (opInfo.opType === 'delete') {
|
||||
await fs.removeFile(file);
|
||||
await handleRefresh();
|
||||
}
|
||||
if (opInfo.opType === 'rowedit') {
|
||||
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||
await updateJson(data);
|
||||
message.success('Name has been changed!');
|
||||
}
|
||||
opInfo.resetRecord();
|
||||
})();
|
||||
}, [opInfo.opType]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (opData?.length === selectedRows.length) {
|
||||
const downloadDir = await path.join(await chatRoot(), 'download');
|
||||
await fs.removeDir(downloadDir, { recursive: true });
|
||||
await handleRefresh();
|
||||
message.success('All files have been cleared!');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = selectedRows.map(async (i) => {
|
||||
const isImg = ['png'].includes(i?.ext);
|
||||
const file = await path.join(
|
||||
await chatRoot(),
|
||||
'download',
|
||||
isImg ? 'img' : i?.ext,
|
||||
`${i?.id}.${i?.ext}`,
|
||||
);
|
||||
await fs.removeFile(file);
|
||||
return file;
|
||||
});
|
||||
Promise.all(rows).then(async () => {
|
||||
await handleRefresh();
|
||||
message.success('All files selected are cleared!');
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' });
|
||||
rowReset();
|
||||
const data = await refreshJson();
|
||||
opInit(data);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
opInfo.resetRecord();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-table-btns">
|
||||
<div>
|
||||
{selectedItems.length > 0 && (
|
||||
<>
|
||||
<Popconfirm
|
||||
overlayStyle={{ width: 250 }}
|
||||
title="Files cannot be recovered after deletion, are you sure you want to delete them?"
|
||||
placement="topLeft"
|
||||
onConfirm={handleDelete}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button>Delete</Button>
|
||||
</Popconfirm>
|
||||
<span className="num">Selected {selectedItems.length} items</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FilePath paths={CHAT_DOWNLOAD_JSON} />
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
scroll={{ x: 800 }}
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
title={<div>{opInfo?.opRecord?.name || ''}</div>}
|
||||
onCancel={handleCancel}
|
||||
footer={false}
|
||||
destroyOnClose
|
||||
>
|
||||
<img style={{ maxWidth: '100%' }} src={source} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/view/markdown/index.scss
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
.md-task {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.ant-breadcrumb-link {
|
||||
padding: 3px 5px;
|
||||
transition: all 300ms ease;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/view/markdown/index.tsx
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import MarkdownEditor from '@/components/Markdown/Editor';
|
||||
import { fs, shell } from '@tauri-apps/api';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import SplitIcon from '@/icons/SplitIcon';
|
||||
import { getPath } from '@/view/notes/config';
|
||||
import './index.scss';
|
||||
|
||||
const modeMap: any = {
|
||||
0: 'split',
|
||||
1: 'md',
|
||||
2: 'doc',
|
||||
};
|
||||
|
||||
export default function Markdown() {
|
||||
const [filePath, setFilePath] = useState('');
|
||||
const [source, setSource] = useState('');
|
||||
const [previewMode, setPreviewMode] = useState(0);
|
||||
const location = useLocation();
|
||||
const state = location?.state;
|
||||
|
||||
useInit(async () => {
|
||||
const file = await getPath(state);
|
||||
setFilePath(file);
|
||||
setSource(await fs.readTextFile(file));
|
||||
});
|
||||
|
||||
const handleChange = async (v: string) => {
|
||||
await fs.writeTextFile(filePath, v);
|
||||
};
|
||||
|
||||
const handlePreview = () => {
|
||||
let mode = previewMode + 1;
|
||||
if (mode > 2) mode = 0;
|
||||
setPreviewMode(mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="md-task">
|
||||
<Breadcrumb separator="">
|
||||
<Breadcrumb.Item onClick={() => history.go(-1)}>
|
||||
<ArrowLeftOutlined />
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item onClick={() => shell.open(filePath)}>{filePath}</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
<div>
|
||||
<SplitIcon onClick={handlePreview} style={{ fontSize: 18, color: 'rgba(0,0,0,0.5)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<MarkdownEditor value={source} onChange={handleChange} mode={modeMap[previewMode]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
128
src/view/model/SyncCustom/Form.tsx
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
ForwardRefRenderFunction,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
import { Form, Input, Select, Tooltip } from 'antd';
|
||||
import { v4 } from 'uuid';
|
||||
import type { FormProps } from 'antd';
|
||||
|
||||
import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
|
||||
import useInit from '@/hooks/useInit';
|
||||
|
||||
interface SyncFormProps {
|
||||
record?: Record<string | symbol, any> | null;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const initFormValue = {
|
||||
act: '',
|
||||
enable: true,
|
||||
tags: [],
|
||||
prompt: '',
|
||||
};
|
||||
|
||||
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record, type }, ref) => {
|
||||
const isDisabled = type === 'edit';
|
||||
const [form] = Form.useForm();
|
||||
useImperativeHandle(ref, () => ({ form }));
|
||||
const [root, setRoot] = useState('');
|
||||
|
||||
useInit(async () => {
|
||||
setRoot(await chatRoot());
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (record) {
|
||||
form.setFieldsValue(record);
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
const pathOptions = (
|
||||
<Form.Item noStyle name="protocol" initialValue="https">
|
||||
<Select disabled={isDisabled}>
|
||||
<Select.Option value="local">{root}</Select.Option>
|
||||
<Select.Option value="http">http://</Select.Option>
|
||||
<Select.Option value="https">https://</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
const extOptions = (
|
||||
<Form.Item noStyle name="ext" initialValue="json">
|
||||
<Select disabled={isDisabled}>
|
||||
<Select.Option value="csv">.csv</Select.Option>
|
||||
<Select.Option value="json">.json</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const jsonTip = (
|
||||
<Tooltip
|
||||
title={
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
[
|
||||
{ cmd: '', act: '', prompt: '' },
|
||||
{ cmd: '', act: '', prompt: '' },
|
||||
],
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
<a>JSON</a>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const csvTip = (
|
||||
<Tooltip
|
||||
title={
|
||||
<pre>{`"cmd","act","prompt"
|
||||
"cmd","act","prompt"
|
||||
"cmd","act","prompt"
|
||||
"cmd","act","prompt"`}</pre>
|
||||
}
|
||||
>
|
||||
<a>CSV</a>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} labelCol={{ span: 4 }} initialValues={initFormValue}>
|
||||
<Form.Item
|
||||
label="Name"
|
||||
name="name"
|
||||
rules={[{ required: true, message: 'Please enter a name!' }]}
|
||||
>
|
||||
<Input placeholder="Please enter a name" {...DISABLE_AUTO_COMPLETE} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="PATH"
|
||||
name="path"
|
||||
rules={[{ required: true, message: 'Please enter the path!' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="YOUR_PATH"
|
||||
addonBefore={pathOptions}
|
||||
addonAfter={extOptions}
|
||||
{...DISABLE_AUTO_COMPLETE}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ display: 'none' }} name="id" initialValue={v4().replace(/-/g, '')}>
|
||||
<input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="tip">
|
||||
<p>
|
||||
The file supports only {csvTip} and {jsonTip} formats.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(SyncForm);
|
||||