Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2bab31cf | ||
|
|
d6fdf41011 | ||
|
|
79bb54db9c | ||
|
|
4a94ee7af8 | ||
|
|
39ff9c1bc2 | ||
|
|
b9699feb3b | ||
|
|
f93df2aea6 | ||
|
|
2c1871d5ba | ||
|
|
e796a19136 | ||
|
|
0d7300be9b | ||
|
|
005d8fb178 | ||
|
|
f53b620df5 | ||
|
|
e706f0ea9e | ||
|
|
e663270072 | ||
|
|
74924d2eea | ||
|
|
a04ea81143 | ||
|
|
e818ee913d | ||
|
|
2a16a64612 | ||
|
|
95e9f05688 | ||
|
|
ee3f6e1d1d | ||
|
|
b7af3595cf | ||
|
|
aa26eea8c5 | ||
|
|
c4be973b78 | ||
|
|
dc743df255 | ||
|
|
319e4f0f95 | ||
|
|
f595cb2aa1 | ||
|
|
894aad9f0b | ||
|
|
5467d550e5 | ||
|
|
d0d0a3d23e | ||
|
|
370dc2dd8a | ||
|
|
d9363b93c6 | ||
|
|
b1b904ce5a | ||
|
|
4564b648f7 | ||
|
|
0fbbe74479 | ||
|
|
8ea98cca5d | ||
|
|
c7c30d8bb5 | ||
|
|
de8f519742 | ||
|
|
af3d74b104 | ||
|
|
2ad675196f | ||
|
|
e434b3afea | ||
|
|
d8ccc5b870 | ||
|
|
7d43032a98 | ||
|
|
5b8483828b | ||
|
|
3c23f16b98 | ||
|
|
7dc479e0a0 | ||
|
|
ee9419cb0b | ||
|
|
1c26bbe43e | ||
|
|
52d57f67aa | ||
|
|
7486a56816 | ||
|
|
40e23b013a | ||
|
|
c119c4044a | ||
|
|
6f037309ad | ||
|
|
af4110ff15 | ||
|
|
55f04ffa60 | ||
|
|
e5cf51b2d6 | ||
|
|
e8e512a451 | ||
|
|
c2967eafa4 | ||
|
|
853b4dfd49 | ||
|
|
e49adaa314 | ||
|
|
394cdcd9f4 | ||
|
|
c5561434c8 | ||
|
|
26e7a715e0 | ||
|
|
b07b74ba54 | ||
|
|
70152174b9 | ||
|
|
8bd29f6d98 | ||
|
|
b8720eec3d | ||
|
|
ad0f2408c8 | ||
|
|
e2ad9accbe | ||
|
|
b496174b4c | ||
|
|
3295eb806c | ||
|
|
34bef48e84 | ||
|
|
a6c93ad681 | ||
|
|
89ab74a913 | ||
|
|
a46ec62532 | ||
|
|
22a967927f | ||
|
|
0aa7581358 | ||
|
|
513cd28528 | ||
|
|
83b88bd759 | ||
|
|
4f18a471b0 | ||
|
|
737abd54ac | ||
|
|
1de51467ec | ||
|
|
dcc0ab98e1 | ||
|
|
730256dcda | ||
|
|
40e793477b | ||
|
|
b856db4772 | ||
|
|
cab1cbceab | ||
|
|
b73be0dcfa | ||
|
|
c6fb3018e7 | ||
|
|
95cf27ee3e | ||
|
|
7afe09fa02 | ||
|
|
bff33c79b3 | ||
|
|
f73936e5f4 | ||
|
|
2b8d37c38f | ||
|
|
ca3da2505a | ||
|
|
d3046dca07 | ||
|
|
25fd39c2b9 | ||
|
|
ab8724b568 | ||
|
|
c240d14864 | ||
|
|
1d464fdcfa | ||
|
|
aacc292522 | ||
|
|
350a1bbae0 | ||
|
|
90b74aff2e | ||
|
|
37f36ec44a | ||
|
|
71b7eaa3f5 | ||
|
|
719413f87a | ||
|
|
97634865eb | ||
|
|
1dbfb0dab7 | ||
|
|
0a671849b5 | ||
|
|
655e7ce6d6 | ||
|
|
68979015c1 | ||
|
|
8f58c95452 | ||
|
|
67161c983f | ||
|
|
73449d9ec6 | ||
|
|
f5d102b7bd | ||
|
|
40ed6fa9ec | ||
|
|
5164cf46ac | ||
|
|
277685c218 | ||
|
|
e25aa74d7b | ||
|
|
47a6cfcafd | ||
|
|
83a96706b4 | ||
|
|
75be4d9722 | ||
|
|
a5cf2f9148 | ||
|
|
8be19f9982 | ||
|
|
36f3d37ecc | ||
|
|
c233cc0d5c | ||
|
|
194051e424 | ||
|
|
94c0fbb525 | ||
|
|
97a6cd801b | ||
|
|
1041146fcb | ||
|
|
e531a17e0f | ||
|
|
30a7a80bfc | ||
|
|
67f8374c9e | ||
|
|
0cc4aea204 | ||
|
|
04796824d5 | ||
|
|
9020239e1f | ||
|
|
0a12b47760 | ||
|
|
9358a4fdb5 | ||
|
|
7d796f2c3e | ||
|
|
0a1651f6a1 | ||
|
|
d13315c45b | ||
|
|
0b75d5d6fe | ||
|
|
39819b744c | ||
|
|
28c8f066d9 | ||
|
|
c85602b93b | ||
|
|
08c91871c7 | ||
|
|
80ca3bc375 | ||
|
|
0405206438 | ||
|
|
0af8f6a699 | ||
|
|
b0936fa322 | ||
|
|
4cd0ff2682 | ||
|
|
4ce60537ca | ||
|
|
0b47218cd5 | ||
|
|
d56aa2edef | ||
|
|
4e6168d8fa | ||
|
|
4197a92609 | ||
|
|
da42d6272a | ||
|
|
b97594c000 | ||
|
|
0f54ffd8b4 | ||
|
|
610cba4a60 | ||
|
|
4f5ee8b198 | ||
|
|
586c162404 | ||
|
|
1308ef1394 | ||
|
|
1513c27f7d | ||
|
|
0ff3bbb28f | ||
|
|
7987c0100c | ||
|
|
e8611a1d07 | ||
|
|
ce78123369 | ||
|
|
a90db1f1a4 | ||
|
|
a213868b17 | ||
|
|
6b2a2bb858 | ||
|
|
fea3afa740 | ||
|
|
7372b37fe6 | ||
|
|
e11ce141d7 | ||
|
|
46fbd3b66a | ||
|
|
ce3f03267a | ||
|
|
9a2392e4d5 | ||
|
|
1eab4d240d | ||
|
|
5568a60174 | ||
|
|
ea4180f22a | ||
|
|
6d2f3361d0 | ||
|
|
9995a159aa | ||
|
|
854f1c3572 | ||
|
|
dcc13daf67 | ||
|
|
2310bab348 | ||
|
|
d64edfdc7d | ||
|
|
a8c53f1f0d | ||
|
|
ef9f1ee1cf | ||
|
|
66ad54168a | ||
|
|
0891566d1e | ||
|
|
e3b0cb7db7 | ||
|
|
c27554ed2e | ||
|
|
87f793f1c4 | ||
|
|
4078c5283b | ||
|
|
5cac7e48f0 | ||
|
|
7a08c77850 | ||
|
|
1fe9e29187 | ||
|
|
ba8692dbe4 | ||
|
|
d30b406c4c | ||
|
|
915cda70ef | ||
|
|
df19595c5b | ||
|
|
867b3073d4 | ||
|
|
e4e28dbbe2 | ||
|
|
cdbc0e21e7 | ||
|
|
b6f7f95709 | ||
|
|
131af50034 | ||
|
|
23c050b54e | ||
|
|
7442294c41 | ||
|
|
a47dbe6262 | ||
|
|
aabb19656e | ||
|
|
b0284b6974 | ||
|
|
62d88380e0 | ||
|
|
41f351786f | ||
|
|
ffcfb69dee | ||
|
|
c91ce36227 | ||
|
|
d06e58f043 | ||
|
|
d052d221dc | ||
|
|
ff45511011 | ||
|
|
8c6340aed0 | ||
|
|
84b104e65f | ||
|
|
a0c94715ce | ||
|
|
a8aad30fc8 | ||
|
|
2fd50c99b8 | ||
|
|
96ca783517 | ||
|
|
45ca0a8713 | ||
|
|
5d0b849930 | ||
|
|
54aa9debb4 | ||
|
|
4e91437049 | ||
|
|
918f2fecb6 | ||
|
|
796d8031e8 | ||
|
|
6e32f71565 | ||
|
|
6192c2964e | ||
|
|
626a8fbd8e | ||
|
|
a8344ec5bf | ||
|
|
c230fe41f4 | ||
|
|
d0ef0f84c8 | ||
|
|
8289558d94 | ||
|
|
2e20b28c4d | ||
|
|
9a17e94f8f | ||
|
|
71fc86b9a6 | ||
|
|
8882432210 | ||
|
|
644f3f716f | ||
|
|
8b00805d24 | ||
|
|
d73375958b | ||
|
|
7168498543 | ||
|
|
27515cb00a | ||
|
|
3e7ce67609 | ||
|
|
4fd05e15b4 | ||
|
|
0fa19bb6ad | ||
|
|
d9e5464b3b | ||
|
|
953c5fc970 | ||
|
|
2afbc5883f | ||
|
|
953f846958 | ||
|
|
a4d5f6a3f2 | ||
|
|
4a39965b22 | ||
|
|
90dc171b34 | ||
|
|
0e98cb4206 | ||
|
|
9f8e9cb091 | ||
|
|
8773878be2 | ||
|
|
5a409ccfa6 | ||
|
|
0ed8a40a41 | ||
|
|
be71140dd4 | ||
|
|
6d51ec3e37 | ||
|
|
bdfc895800 | ||
|
|
b9975ac283 | ||
|
|
dd1f74da72 | ||
|
|
5d65427a6e | ||
|
|
6a16d31f26 | ||
|
|
8affbfdb4a | ||
|
|
70d59c5b3c | ||
|
|
73979ee67f | ||
|
|
8e513d83a5 | ||
|
|
9bc85ea83d | ||
|
|
943eb5c74d | ||
|
|
bac37cfe36 | ||
|
|
b2334054da | ||
|
|
a705e907ff | ||
|
|
c8bb6b13bf | ||
|
|
50a17bc1b8 | ||
|
|
3038015a5b | ||
|
|
20da895ee7 | ||
|
|
8762765dd0 | ||
|
|
f1aabfa543 | ||
|
|
23de688bf3 | ||
|
|
79f050bac7 | ||
|
|
afae13eec6 | ||
|
|
4fa599e97a | ||
|
|
3b1d6aaa79 | ||
|
|
339230cd74 | ||
|
|
95bad60f7d | ||
|
|
950b8f3f1e | ||
|
|
0e2e5b8393 | ||
|
|
d2cb9957fe | ||
|
|
78b4004ead | ||
|
|
8624062488 | ||
|
|
070fee2ece | ||
|
|
b1569450fd | ||
|
|
09659be002 | ||
|
|
5e4fa09dcb | ||
|
|
16c9589058 | ||
|
|
8604030404 | ||
|
|
97668217b9 | ||
|
|
c5c865a25f | ||
|
|
06c99154ac | ||
|
|
9d71b58345 | ||
|
|
fb9f77ae5e | ||
|
|
fc2b9bf7f2 | ||
|
|
cdbe00ec6f | ||
|
|
72ff47e204 | ||
|
|
57d3025717 | ||
|
|
6c02558f1b | ||
|
|
0c62863e52 | ||
|
|
d825721dad | ||
|
|
0b3b6f91fc | ||
|
|
3a9b532248 | ||
|
|
67156f4a7a | ||
|
|
4e616cd2ed | ||
|
|
12cf3405e4 | ||
|
|
4383894ad3 | ||
|
|
d0ecaabfd8 | ||
|
|
a286f027c8 | ||
|
|
0ba92c4c03 | ||
|
|
30936573ac | ||
|
|
a451574760 | ||
|
|
5e57deab5f | ||
|
|
eae82edb83 | ||
|
|
1e0cbbf1cc | ||
|
|
3986b4ec9e | ||
|
|
67054e7504 | ||
|
|
2c1ae68dc4 | ||
|
|
69b3edc52c | ||
|
|
732c75d145 | ||
|
|
0f54251459 | ||
|
|
72ef76b8a0 | ||
|
|
81abbdb335 | ||
|
|
67ca79dd3f | ||
|
|
5af03bb3d8 | ||
|
|
e5d25f45e3 | ||
|
|
ad967beaf8 | ||
|
|
a8eaf2452a | ||
|
|
5ad32e211f | ||
|
|
da61a2841f | ||
|
|
da55ac3774 | ||
|
|
f2b092545f | ||
|
|
17c01aa252 | ||
|
|
6be037323e | ||
|
|
191118b90b | ||
|
|
6d757ff91f | ||
|
|
d93282640c | ||
|
|
b9e9203a75 | ||
|
|
ee8eba9ad5 | ||
|
|
102327313e | ||
|
|
08f59819cf | ||
|
|
d9930dd2bc | ||
|
|
453301db0e | ||
|
|
52e529dcbe | ||
|
|
ff7b016190 | ||
|
|
f5e079742a | ||
|
|
affbaaf1a5 | ||
|
|
fca546af63 | ||
|
|
062cd1d838 | ||
|
|
6ae154cc42 | ||
|
|
c617b15bcd | ||
|
|
b750bd2970 | ||
|
|
8d60cbdc7a | ||
|
|
15986c078b | ||
|
|
15f3972b9f | ||
|
|
3224dca3b8 | ||
|
|
a98367d27f | ||
|
|
80319c7fcc | ||
|
|
dae7f8d822 | ||
|
|
c8d8484d86 | ||
|
|
f40f80f030 | ||
|
|
fc899da944 | ||
|
|
d64f914552 | ||
|
|
b4b0c123ba | ||
|
|
9c3a78f96b | ||
|
|
62bb6ea8f8 | ||
|
|
0bec306e59 | ||
|
|
42c74f254c | ||
|
|
9a56130750 | ||
|
|
214228542a | ||
|
|
0f9cfd0395 | ||
|
|
92648554ad | ||
|
|
ac2229fbb6 | ||
|
|
ec7aaf01a4 | ||
|
|
50ff96cbc5 | ||
|
|
0d8755a03a | ||
|
|
d235bb7b1b | ||
|
|
c4d0787b49 | ||
|
|
897c384ac9 | ||
|
|
cbcc1a1bb4 | ||
|
|
1f270d349a | ||
|
|
90ac43c8c5 | ||
|
|
1bba86f2e2 | ||
|
|
8e5148b390 | ||
|
|
0ee31f0443 | ||
|
|
ed699d2c06 | ||
|
|
0f6fcd5928 | ||
|
|
78c9f3dc74 | ||
|
|
34ebd97c9d | ||
|
|
61d898c387 | ||
|
|
fd01fd3334 | ||
|
|
c58a9bbe93 | ||
|
|
e95e22de15 | ||
|
|
187f7b5b03 | ||
|
|
1a6cddb8bb | ||
|
|
e2a3ecbf88 | ||
|
|
a516b38e27 | ||
|
|
16932b37c0 | ||
|
|
168d5e8075 | ||
|
|
7b7548c905 | ||
|
|
81e52fed73 | ||
|
|
b4e22936ba | ||
|
|
ac2e897709 | ||
|
|
5c7000c273 | ||
|
|
e0a26f52cd | ||
|
|
f18317f7b7 | ||
|
|
7347eda219 | ||
|
|
e1b615835a | ||
|
|
eaa0242e10 | ||
|
|
481927c5a3 | ||
|
|
0833d795bd | ||
|
|
7d7a4dd905 | ||
|
|
e520973203 | ||
|
|
9c225a9bb3 | ||
|
|
742073fb89 | ||
|
|
e4d1ff2523 | ||
|
|
c00a2c902b | ||
|
|
a5afd5c48f | ||
|
|
779f142058 | ||
|
|
92b2109dc3 | ||
|
|
583fd56ca5 | ||
|
|
acaef39d12 | ||
|
|
dcf4f2a524 | ||
|
|
0e5c7c7721 | ||
|
|
500b6c9fa8 | ||
|
|
5b8c4452cc | ||
|
|
ed44daf8b3 | ||
|
|
cd71bb0727 | ||
|
|
48c591a8b9 | ||
|
|
8eeac36b35 | ||
|
|
4cecbcf642 | ||
|
|
8432cbd293 | ||
|
|
256b60435f | ||
|
|
d9d5f4a509 | ||
|
|
e8c514fff9 | ||
|
|
ef4ec39a76 | ||
|
|
4304597cb4 | ||
|
|
7dd4e78bbf | ||
|
|
5af5a97d8f | ||
|
|
592b7629aa | ||
|
|
cab4b70b7e | ||
|
|
c36f752551 | ||
|
|
217bdb4865 | ||
|
|
87685c3791 | ||
|
|
0c6dc8665b | ||
|
|
5cd50e7826 | ||
|
|
581ee0e2ca | ||
|
|
d07047722e | ||
|
|
b5042a738e | ||
|
|
58498ed951 | ||
|
|
51d93da3f8 | ||
|
|
511ac948b4 | ||
|
|
053368646d | ||
|
|
6842ac880c | ||
|
|
9d41ed4615 | ||
|
|
faf8800e67 | ||
|
|
36ac055ae5 | ||
|
|
7978ddd871 | ||
|
|
85efaa4173 | ||
|
|
6e3f63ee46 | ||
|
|
c7c50dbbab | ||
|
|
d35ed31b20 | ||
|
|
c794287ced | ||
|
|
2869638cc0 | ||
|
|
c821d1eee5 | ||
|
|
4e6b6be15c | ||
|
|
511c8c9599 | ||
|
|
232a823b9b | ||
|
|
f2003da779 | ||
|
|
254f9d7e26 | ||
|
|
f889f23792 | ||
|
|
3a199757ae | ||
|
|
27f7276fc8 | ||
|
|
81866f197a | ||
|
|
502bdc5dd5 | ||
|
|
641dcfad73 | ||
|
|
7c4e7ab07a | ||
|
|
7b9f3ee9c2 | ||
|
|
39781f5318 | ||
|
|
521a8eb704 | ||
|
|
92860a1bb4 | ||
|
|
037a12a282 | ||
|
|
5dc2d5efba | ||
|
|
21a6a0587c | ||
|
|
0fb3a1cab3 | ||
|
|
74232d7671 | ||
|
|
466dd01327 |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "Frappe Bench",
|
||||
"forwardPorts": [
|
||||
8000,
|
||||
9000,
|
||||
6787
|
||||
],
|
||||
"remoteUser": "frappe",
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"debug.node.autoAttach": "disabled"
|
||||
},
|
||||
"dockerComposeFile": "./docker-compose.yml",
|
||||
"service": "frappe",
|
||||
"workspaceFolder": "/workspace/frappe-bench",
|
||||
"postCreateCommand": "bash /workspace/scripts/init.sh",
|
||||
"shutdownAction": "stopCompose",
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-vscode.live-server",
|
||||
"grapecity.gc-excelviewer",
|
||||
"mtxr.sqltools",
|
||||
"visualstudioexptteam.vscodeintellicode"
|
||||
]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:10.6
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --skip-character-set-client-handshake
|
||||
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 123
|
||||
volumes:
|
||||
- mariadb-data:/var/lib/mysql
|
||||
|
||||
# Enable PostgreSQL only if you use it, see development/README.md for more information.
|
||||
# postgresql:
|
||||
# image: postgres:11.8
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: 123
|
||||
# volumes:
|
||||
# - postgresql-data:/var/lib/postgresql/data
|
||||
|
||||
redis-cache:
|
||||
image: redis:alpine
|
||||
|
||||
redis-queue:
|
||||
image: redis:alpine
|
||||
|
||||
redis-socketio:
|
||||
image: redis:alpine
|
||||
|
||||
frappe:
|
||||
image: frappe/bench:latest
|
||||
command: sleep infinity
|
||||
environment:
|
||||
- SHELL=/bin/bash
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
working_dir: /workspace/frappe-bench
|
||||
ports:
|
||||
- 8000-8005:8000-8005
|
||||
- 9000-9005:9000-9005
|
||||
|
||||
volumes:
|
||||
mariadb-data:
|
||||
postgresql-data:
|
||||
@@ -0,0 +1,2 @@
|
||||
**/node_modules
|
||||
**/.env
|
||||
@@ -1,38 +0,0 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
E121,
|
||||
E126,
|
||||
E127,
|
||||
E128,
|
||||
E203,
|
||||
E225,
|
||||
E226,
|
||||
E231,
|
||||
E241,
|
||||
E251,
|
||||
E261,
|
||||
E265,
|
||||
E302,
|
||||
E303,
|
||||
E305,
|
||||
E402,
|
||||
E501,
|
||||
E741,
|
||||
W291,
|
||||
W292,
|
||||
W293,
|
||||
W391,
|
||||
W503,
|
||||
W504,
|
||||
F403,
|
||||
F811,
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
E131, # continuation line unaligned for hanging indent
|
||||
E123, # closing bracket does not match indentation of opening bracket's line
|
||||
E101, # ensured by use of black
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [The-Commit-Company]
|
||||
github: [danny-avila]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
@@ -8,7 +8,6 @@ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B001,
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
E114,
|
||||
E116,
|
||||
E117,
|
||||
E121,
|
||||
E122,
|
||||
E123,
|
||||
E124,
|
||||
E125,
|
||||
E126,
|
||||
E127,
|
||||
E128,
|
||||
E131,
|
||||
E201,
|
||||
E202,
|
||||
E203,
|
||||
E211,
|
||||
E221,
|
||||
E222,
|
||||
E223,
|
||||
E224,
|
||||
E225,
|
||||
E226,
|
||||
E228,
|
||||
E231,
|
||||
E241,
|
||||
E242,
|
||||
E251,
|
||||
E261,
|
||||
E262,
|
||||
E265,
|
||||
E266,
|
||||
E271,
|
||||
E272,
|
||||
E273,
|
||||
E274,
|
||||
E301,
|
||||
E302,
|
||||
E303,
|
||||
E305,
|
||||
E306,
|
||||
E402,
|
||||
E501,
|
||||
E502,
|
||||
E701,
|
||||
E702,
|
||||
E703,
|
||||
E741,
|
||||
F401,
|
||||
F403,
|
||||
F405,
|
||||
F811,
|
||||
W191,
|
||||
W291,
|
||||
W292,
|
||||
W293,
|
||||
W391,
|
||||
W503,
|
||||
W504,
|
||||
E711,
|
||||
E129,
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd ~ || exit
|
||||
|
||||
echo "Setting Up Bench..."
|
||||
|
||||
pip install frappe-bench
|
||||
bench -v init frappe-bench --skip-assets --python "$(which python)"
|
||||
cd ./frappe-bench || exit
|
||||
|
||||
bench -v setup requirements
|
||||
|
||||
echo "Setting Up Raven..."
|
||||
bench get-app raven "${GITHUB_WORKSPACE}"
|
||||
|
||||
echo "Setting Up Sites & Database..."
|
||||
|
||||
mkdir ~/frappe-bench/sites/raven.test
|
||||
cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/raven.test/site_config.json
|
||||
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_raven";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_raven'@'localhost' IDENTIFIED BY 'test_raven'";
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_raven\`.* TO 'test_raven'@'localhost'";
|
||||
|
||||
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES";
|
||||
|
||||
echo "Setting Up Procfile..."
|
||||
|
||||
sed -i 's/^watch:/# watch:/g' Procfile
|
||||
sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
echo "Starting Bench..."
|
||||
|
||||
bench start &> bench_start.log &
|
||||
|
||||
CI=Yes bench build &
|
||||
build_pid=$!
|
||||
|
||||
bench --site raven.test reinstall --yes
|
||||
bench --site raven.test install-app raven
|
||||
|
||||
wait $build_pid
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Setting Up System Dependencies..."
|
||||
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client
|
||||
|
||||
install_wkhtmltopdf() {
|
||||
wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb
|
||||
}
|
||||
install_wkhtmltopdf &
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 3306,
|
||||
"db_name": "test_raven",
|
||||
"db_password": "test_raven",
|
||||
"allow_tests": true,
|
||||
"db_type": "mariadb",
|
||||
"auto_email_id": "test@example.com",
|
||||
"mail_server": "smtp.example.com",
|
||||
"mail_login": "test@example.com",
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"root_login": "root",
|
||||
"root_password": "123",
|
||||
"host_name": "http://raven.test:8000",
|
||||
"monitor": 1,
|
||||
"server_script_enabled": true,
|
||||
"mute_emails": true
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# Add 'UI' label to all PRs that touch ts / tsx files in the 'src' directory.
|
||||
"UI":
|
||||
- "frontend/src/**/*.{ts,tsx,css}"
|
||||
|
||||
# Add 'backend' label to all PRs that touch json / py files in the 'raven' directory.
|
||||
"backend":
|
||||
- "raven/**/*.{json,py}"
|
||||
@@ -1,11 +0,0 @@
|
||||
# configuration to force mariadb to use utf8mb4 charecter set, as required by frappe
|
||||
# This file need to be placed at /etc/mysql/conf.d/ in the mariadb container as a volume
|
||||
# See .github/wotkflows/ci.yml to see how it is used
|
||||
|
||||
[mysqld]
|
||||
character-set-client-handshake = FALSE
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
[mysql]
|
||||
default-character-set = utf8mb4
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Build Container Image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set Branch
|
||||
run: |
|
||||
export APPS_JSON='[{"url": "https://github.com/The-Commit-Company/raven","branch": "main"}]'
|
||||
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
|
||||
echo "FRAPPE_BRANCH=version-15" >> $GITHUB_ENV
|
||||
|
||||
- name: Set Image Tag
|
||||
run: |
|
||||
echo "IMAGE_TAG=stable" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: frappe/frappe_docker
|
||||
path: builds
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: builds
|
||||
file: builds/images/layered/Containerfile
|
||||
tags: >
|
||||
ghcr.io/${{ github.repository }}:${{ github.ref_name }},
|
||||
ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
|
||||
build-args: |
|
||||
"FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
|
||||
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
|
||||
@@ -1,100 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- raven/**
|
||||
pull_request:
|
||||
paths:
|
||||
- raven/**
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: Server
|
||||
|
||||
services:
|
||||
redis-cache:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- 13000:6379
|
||||
redis-queue:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- 11000:6379
|
||||
redis-socketio:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- 12000:6379
|
||||
mariadb:
|
||||
image: anandology/mariadb-utf8mb4:10.3
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT'
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup
|
||||
run: |
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
pip install frappe-bench
|
||||
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
- name: Install
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: |
|
||||
bench get-app https://github.com/The-Commit-Company/Raven.git $GITHUB_WORKSPACE
|
||||
bench setup requirements
|
||||
bench new-site --db-root-password root --admin-password admin test_site
|
||||
bench --site test_site install-app raven
|
||||
bench build
|
||||
env:
|
||||
CI: "Yes"
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: |
|
||||
bench --site test_site set-config allow_tests true
|
||||
bench --site test_site run-tests --app raven
|
||||
env:
|
||||
TYPE: server
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Linters
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- raven/**
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- raven/**
|
||||
|
||||
jobs:
|
||||
linters:
|
||||
name: Semantic Commits
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules
|
||||
+53
-16
@@ -1,19 +1,56 @@
|
||||
.DS_Store
|
||||
*.pyc
|
||||
*.egg-info
|
||||
*.swp
|
||||
tags
|
||||
node_modules
|
||||
### node etc ###
|
||||
|
||||
# Logs
|
||||
data-node
|
||||
meili_data
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Compiled Dirs (http://nodejs.org/api/addons.html)
|
||||
build/
|
||||
dist/
|
||||
mobile/dev-dist
|
||||
raven/public/raven
|
||||
raven/public/raven_v3
|
||||
raven/public/node_modules
|
||||
raven/www/raven.html
|
||||
raven/www/raven_v3.html
|
||||
*.env
|
||||
public/main.js
|
||||
public/main.js.map
|
||||
public/main.js.LICENSE.txt
|
||||
client/public/main.js
|
||||
client/public/main.js.map
|
||||
client/public/main.js.LICENSE.txt
|
||||
|
||||
apps/mobile/ios
|
||||
apps/mobile/android
|
||||
apps/mobile/.expo
|
||||
# Dependency directorys
|
||||
# Deployed apps should consider commenting these lines out:
|
||||
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
||||
node_modules/
|
||||
meili_data/
|
||||
api/node_modules/
|
||||
client/node_modules/
|
||||
bower_components/
|
||||
|
||||
# Floobits
|
||||
.floo
|
||||
.floobit
|
||||
.floo
|
||||
.flooignore
|
||||
|
||||
# Environment
|
||||
.env
|
||||
cache.json
|
||||
api/data/
|
||||
owner.yml
|
||||
archive
|
||||
.vscode/settings.json
|
||||
|
||||
src/style - official.css
|
||||
@@ -1,52 +0,0 @@
|
||||
exclude: "node_modules|.git"
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "raven.*"
|
||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
|
||||
- id: check-yaml
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
files: "raven.*"
|
||||
- id: check-toml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.34.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
files: "raven.*"
|
||||
args: ["--py310-plus"]
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
files: "raven.*"
|
||||
additional_dependencies: ["click==8.0.4"]
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: "raven.*"
|
||||
additional_dependencies: ["flake8-bugbear", "flake8-tuple"]
|
||||
args: ["--config", ".github/helper/flake8.conf"]
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
files: "raven.*"
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
submodules: false
|
||||
@@ -1,2 +0,0 @@
|
||||
.devcontainer/docker-compose.yml
|
||||
frontend/
|
||||
Vendored
-88
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Bench Web",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
|
||||
"args": [
|
||||
"frappe",
|
||||
"serve",
|
||||
"--port",
|
||||
"8000",
|
||||
"--noreload",
|
||||
"--nothreading"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/frappe-bench/sites",
|
||||
"env": {
|
||||
"DEV_SERVER": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bench Default Worker",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
|
||||
"args": [
|
||||
"frappe",
|
||||
"worker",
|
||||
"--queue",
|
||||
"default"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/frappe-bench/sites",
|
||||
"env": {
|
||||
"DEV_SERVER": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bench Short Worker",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
|
||||
"args": [
|
||||
"frappe",
|
||||
"worker",
|
||||
"--queue",
|
||||
"short"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/frappe-bench/sites",
|
||||
"env": {
|
||||
"DEV_SERVER": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bench Long Worker",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/frappe-bench/apps/frappe/frappe/utils/bench_helper.py",
|
||||
"args": [
|
||||
"frappe",
|
||||
"worker",
|
||||
"--queue",
|
||||
"long"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/frappe-bench/sites",
|
||||
"env": {
|
||||
"DEV_SERVER": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Honcho SocketIO Watch Schedule Worker",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "/home/frappe/.local/bin/honcho",
|
||||
"cwd": "${workspaceFolder}/frappe-bench",
|
||||
"console": "internalConsole",
|
||||
"args": [
|
||||
"start",
|
||||
"socketio",
|
||||
"watch",
|
||||
"schedule",
|
||||
"worker_short",
|
||||
"worker_long",
|
||||
"worker_default"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "frappe-bench/env/bin/python",
|
||||
"debug.node.autoAttach": "disabled",
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"mysqlOptions": {
|
||||
"authProtocol": "default"
|
||||
},
|
||||
"previewLimit": 50,
|
||||
"server": "mariadb",
|
||||
"port": 3306,
|
||||
"driver": "MariaDB",
|
||||
"name": "MariaDB",
|
||||
"username": "root",
|
||||
"password": "123"
|
||||
}
|
||||
]
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
FROM node:19-alpine AS react-client
|
||||
WORKDIR /client
|
||||
# copy package.json into the container at /client
|
||||
COPY /client/package*.json /client/
|
||||
# install dependencies
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /client
|
||||
COPY /client/ /client/
|
||||
# Build webpack artifacts
|
||||
RUN npm run build
|
||||
|
||||
FROM node:19-alpine AS node-api
|
||||
WORKDIR /api
|
||||
# copy package.json into the container at /api
|
||||
COPY /api/package*.json /api/
|
||||
# install dependencies
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /api
|
||||
COPY /api/ /api/
|
||||
# Copy the client side code
|
||||
COPY --from=react-client /client/public /client/public
|
||||
# Make port 3080 available to the world outside this container
|
||||
EXPOSE 3080
|
||||
# Expose the server to 0.0.0.0
|
||||
ENV HOST=0.0.0.0
|
||||
# Run the app when the container launches
|
||||
CMD ["npm", "start"]
|
||||
|
||||
# Optional: for client with nginx routing
|
||||
FROM nginx:stable-alpine AS nginx-client
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=react-client /client/public /usr/share/nginx/html
|
||||
# Add your nginx.conf
|
||||
COPY /client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,661 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Danny Avila
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,87 @@
|
||||
### Local
|
||||
- **Install the prerequisites**
|
||||
- **Download chatgpt-clone**
|
||||
- Download the latest release here: https://github.com/danny-avila/chatgpt-clone/releases/
|
||||
- Or by clicking on the green code button in the top of the page and selecting "Download ZIP"
|
||||
- Or (Recommended if you have Git installed) pull the latest release from the main branch
|
||||
- If you downloaded a zip file, extract the content in "C:/chatgpt-clone/"
|
||||
-**IMPORTANT : If you install the files somewhere else modify the instructions accordingly**
|
||||
|
||||
- **To enable the Conversation search feature:**
|
||||
-IF YOU DON'T WANT THIS FEATURE YOU CAN SKIP THIS STEP
|
||||
- Download MeileSearch latest release from : https://github.com/meilisearch/meilisearch/releases
|
||||
- Copy it to "C:/chatgpt-clone/"
|
||||
- Rename the file to "meilisearch.exe"
|
||||
- Open it by double clicking on it
|
||||
- Copy the generated Master Key and save it somewhere (You will need it later)
|
||||
|
||||
- **Download and Install Node.js**
|
||||
- Navigate to https://nodejs.org/en/download and to download the latest Node.js version for your OS (The Node.js installer includes the NPM package manager.)
|
||||
|
||||
- **Create a MongoDB database**
|
||||
- Navigate to https://www.mongodb.com/ and Sign In or Create an account
|
||||
- Create a new project
|
||||
- Build a Database using the free plan and name the cluster (example: chatgpt-clone)
|
||||
- Use the "Username and Password" method for authentication
|
||||
- Add your current IP to the access list
|
||||
- Then in the Database Deployment tab click on Connect
|
||||
- In "Choose a connection method" select "Connect your application"
|
||||
- Driver = Node.js / Version = 4.1 or later
|
||||
- Copy the connection string and save it somewhere(you will need it later)
|
||||
|
||||
- **Get your OpenAI API key** here: https://platform.openai.com/account/api-keys and save it somewhere safe (you will need it later)
|
||||
|
||||
- **Get your Bing Access Token**
|
||||
- Using MS Edge, navigate to bing.com
|
||||
- Make sure you are logged in
|
||||
- Open the DevTools by pressing F12 on your keyboard
|
||||
- Click on the tab "Application" (On the left of the DevTools)
|
||||
- Expand the "Cookies" (Under "Storage")
|
||||
- You need to copy the value of the "_U" cookie, save it somewhere, you will need it later
|
||||
|
||||
- **Create the ".env" File** You will need all your credentials, (API keys, access tokens, and Mongo Connection String, MeileSearch Master Key)
|
||||
- Open "C:/chatgpt-clone/api/.env.example" in a text editor
|
||||
- At this line **MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"**
|
||||
Replace mongodb://127.0.0.1:27017/chatgpt-clone with the MondoDB connection string you saved earlier, **remove "&w=majority" at the end**
|
||||
- It should look something like this: "MONGO_URI="mongodb+srv://username:password@chatgpt-clone.lfbcwz3.mongodb.net/?retryWrites=true"
|
||||
- At this line **OPENAI_KEY=** you need to add your openai API key
|
||||
- Add your Bing token to this line **BING_TOKEN=** (needed for BingChat & Sydney)
|
||||
- If you want to enable Search, **SEARCH=TRUE** if you do not want to enable search **SEARCH=FALSE**
|
||||
- Add your previously saved MeiliSearch Master key to this line **MEILI_MASTER_KEY=** (the key is needed if search is enabled even on local install or you may encounter errors)
|
||||
- Save the file as **"C:/chatgpt-clone/api/.env"**
|
||||
|
||||
**DO THIS ONCE AFTER EVERY UPDATE**
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/api" directory
|
||||
- **Run** `npm ci` in the "C:/chatgpt-clone/client" directory
|
||||
- **Run** `npm run build` in the "C:/chatgpt-clone/client"
|
||||
|
||||
**DO THIS EVERY TIME YOU WANT TO START CHATGPT-CLONE**
|
||||
- **Run** `"meilisearch --master-key put_your_meilesearch_Master_Key_here"` in the "C:/chatgpt-clone" directory (Only if SEARCH=TRUE)
|
||||
- **Run** `npm start` in the "C:/chatgpt-clone/api" directory
|
||||
|
||||
- **Visit** http://localhost:3080 (default port) & enjoy
|
||||
|
||||
|
||||
OPTIONAL BUT RECOMMENDED
|
||||
- **Make a batch file to automate the starting process**
|
||||
- Open a text editor
|
||||
- Paste the following code in a new document
|
||||
- Put your MeiliSearch master key instead of "your_master_key_goes_here"
|
||||
- Save the file as "C:/chatgpt-clone/chatgpt-clone.bat"
|
||||
- you can make a shortcut of this batch file and put it anywhere
|
||||
|
||||
```
|
||||
REM the meilisearch executable needs to be at the root of the chatgpt-clone directory
|
||||
|
||||
start "MeiliSearch" cmd /k "meilisearch --master-key your_master_key_goes_here
|
||||
|
||||
REM ↑↑↑ meilisearch is the name of the meilisearch executable, put your own master key there
|
||||
|
||||
start "ChatGPT-Clone" cmd /k "cd api && npm start"
|
||||
|
||||
REM this batch file goes at the root of the chatgpt-clone directory (C:/chatgpt-clone/)
|
||||
```
|
||||
|
||||
If you update the chatgpt-clone project files, mannually redo the `npm ci` and `npm run build` steps
|
||||
|
||||
To share within network or serve as a public server, set `HOST` to `0.0.0.0` in `.env` file.
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include *.json
|
||||
include *.md
|
||||
include *.py
|
||||
include *.txt
|
||||
recursive-include raven *.css
|
||||
recursive-include raven *.csv
|
||||
recursive-include raven *.html
|
||||
recursive-include raven *.ico
|
||||
recursive-include raven *.js
|
||||
recursive-include raven *.json
|
||||
recursive-include raven *.md
|
||||
recursive-include raven *.png
|
||||
recursive-include raven *.py
|
||||
recursive-include raven *.svg
|
||||
recursive-include raven *.txt
|
||||
recursive-exclude raven *.pyc
|
||||
@@ -1,271 +1,395 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/The-Commit-Company/raven">
|
||||
<img src="raven_logo.png" alt="Raven logo" height="100" />
|
||||
</a>
|
||||
<hr />
|
||||
<p align="center">Enterprise-first messaging platform that seamlessly integrates with your ERP
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://frappecloud.com/dashboard/signup?product=raven"><strong>Install on Frappe Cloud</strong></a>
|
||||
<br />
|
||||
<a href="https://ravenchat.ai">Website (ravenchat.ai)</a>
|
||||
<br />
|
||||
<a href="https://github.com/The-Commit-Company/raven/issues">Issues</a>
|
||||
·
|
||||
<a href="https://community.ravenapp.cloud">Community & Support</a>
|
||||
·
|
||||
<a href="https://github.com/sponsors/The-Commit-Company?frequency=one-time">Sponsor Us!</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/The-Commit-Company/Raven/blob/main/LICENSE">
|
||||
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue">
|
||||
<a href="https://discord.gg/sDfH4MwDWJ">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png">
|
||||
<img src="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png" height="128">
|
||||
</picture>
|
||||
<h1 align="center">ChatGPT Clone</h1>
|
||||
</a>
|
||||
<a href="https://github.com/The-Commit-Company/raven/stargazers"><img src="https://img.shields.io/github/stars/The-Commit-Company/raven" alt="Github Stars"></a>
|
||||
<a href="https://github.com/The-Commit-Company/raven/pulse"><img src="https://img.shields.io/github/commit-activity/m/The-Commit-Company/raven" alt="Commits-per-month"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="screenshots/RavenUIGraphic.png" alt="A graphic showing how the Raven interface looks on the web" />
|
||||
<a aria-label="Join the community on Discord" href="https://discord.gg/sDfH4MwDWJ">
|
||||
<img alt="" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=DISCORD&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
<a aria-label="Sponsors" href="#sponsors">
|
||||
<img alt="" src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## All AI Conversations under One Roof. ##
|
||||
Assistant AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous UIs exist, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future AI models, while integrating and improving upon original client features, such as conversation/message search and prompt templates (currently WIP). Through this clone, you can avoid ChatGPT Plus in favor of free or pay-per-call APIs. I will soon deploy a demo of this app. Feel free to contribute, clone, or fork. Currently dockerized.
|
||||
|
||||
<div align="center">
|
||||
<video src="https://user-images.githubusercontent.com/110412045/223754183-8b7f45ce-6517-4bd5-9b39-c624745bf399.mp4" width=400/>
|
||||
</div>
|
||||
|
||||
Raven is an open-source messaging platform that brings your team's conversations and information into one centralized place, enhancing collaboration and productivity. Whether you're at your desk or on the move, Raven keeps you connected on any device.
|
||||
## Sponsors
|
||||
|
||||
It's built using [Frappe Framework](https://frappeframework.com) and offers deep integrations with other Frappe apps like [ERPNext](https://frappe.io/erpnext) and [FrappeHR](https://frappe.io/hr).
|
||||
Sponsored by <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>
|
||||
|
||||
<hr>
|
||||
|
||||
## Key Features
|
||||
## Updates
|
||||
<details open>
|
||||
<summary><strong>2023-03-23</strong></summary>
|
||||
|
||||
1. **Organize conversations in channels:** Organize your conversations by topics, projects, or any category that suits your workflow. Send direct messages to other users or create channels for group discussions to keep everyone informed and engaged.
|
||||
2. **Craft your messages with rich text:** Easily send messages with formatting, highlights, links, emojis, code blocks, mentions and more
|
||||
3. **Share images, documents, GIFs and more:** Need to share a file? Simply drag and drop it onto the screen. You can also find and share memes using our GIF search
|
||||
4. **Reactions, Replies, and Threads:** Respond to messages with a reaction (custom emojis supported), inline replies or create threads for longer discussions.
|
||||
5. **Integrate Seamlessly:** Raven works effortlessly with other Frappe apps, allowing you to share documents from ERPNext with customizable previews, trigger notifications based on document events, perform workflow actions and, print documents directly within chats.
|
||||
6. **Agents, AI and more:** With Raven AI, automate tasks, extract data from files and images, and execute complex, multistep processes with just a message to an agent. Build your own agents without writing a single line of code to streamline your workflows.
|
||||
7. **Supports Dark Mode and Mobile**: Raven allows users to customize the app's appearance as well as offers a mobile app.
|
||||
8. **Authentication and Security**: Supports all popular OAuth providers, custom OAuth providers, Login with email link, 2FA on Email/SMS/Auth App and LDAP.
|
||||
|
||||
<hr>
|
||||
|
||||
**Released [v0.1.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)**, **searching messages/conversations is live!** Up next is more custom parameters for customGpt's. Join the discord server for more immediate assistance and update: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Previous Updates</strong></summary>
|
||||
|
||||
<details>
|
||||
<summary><strong>2023-03-22</strong></summary>
|
||||
|
||||
|
||||
|
||||
**Released [v0.0.6](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.6)**, the latest stable release before **Searching messages** goes live tomorrow. See exact updates to date in the tag link. By request, there is now also a **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-20</strong></summary>
|
||||
|
||||
|
||||
|
||||
**Searching messages** is almost here as I test more of its functionality. There've been a lot of great features requested and great contributions and I will work on some soon, namely, further customizing the custom gpt params with sliders similar to the OpenAI playground, and including the custom params and system messages available to Bing.
|
||||
|
||||
The above features are next and then I will have to focus on building the **test environment.** I would **greatly appreciate** help in this area with any test environment you're familiar with (mocha, chai, jest, playwright, puppeteer). This is to aid in the velocity of contributing and to save time I spend debugging.
|
||||
|
||||
On that note, I had to switch the default branch due to some breaking changes that haven't been straight forward to debug, mainly related to node-chat-gpt the main dependency of the project. Thankfully, my working branch, now switched to default as main, is working as expected.
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-16</strong></summary>
|
||||
|
||||
|
||||
|
||||
[Latest release (v0.0.4)](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.4) includes Resubmitting messages & Branching messages, which mirrors official ChatGPT feature of editing a sent message, that then branches the conversation into separate message paths (works only with ChatGPT)
|
||||
|
||||
Full details and [example here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.4). Message search is on the docket
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-12</strong></summary>
|
||||
|
||||
|
||||
|
||||
|
||||
Really thankful for all the issues reported and contributions made, the project's features and improvements have accelerated as result. Honorable mention is [wtlyu](https://github.com/wtlyu) for contributing a lot of mindful code, namely hostname configuration and mobile styling. I will upload images on next release for faster docker setup, and starting updating them simultaneously with this repo.
|
||||
|
||||
|
||||
|
||||
Many improvements across the board, the biggest is being able to start conversations simultaneously (again thanks to [wtlyu](https://github.com/wtlyu) for bringing it to my attention), as you can switch conversations or start a new chat without any response streaming from a prior one, as the backend will still process/save client responses. Just watch out for any rate limiting from OpenAI/Microsoft if this is done excessively.
|
||||
|
||||
|
||||
Adding support for conversation search is next! Thank you [mysticaltech](https://github.com/mysticaltech) for bringing up a method I can use for this.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-09</strong></summary>
|
||||
Released v.0.0.2
|
||||
|
||||
Adds Sydney (jailbroken Bing AI) to the model menu. Thank you [DavesDevFails](https://github.com/DavesDevFails) for bringing it to my attention in this [issue](https://github.com/danny-avila/chatgpt-clone/issues/13). Bing/Sydney now correctly cite links, more styling to come. Fix some overlooked bugs, and model menu doesn't close upon deleting a customGpt.
|
||||
|
||||
|
||||
I've re-enabled the ChatGPT browser client (free version) since it might be working for most people, it no longer works for me. Sydney is the best free route anyway.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-07</strong></summary>
|
||||
Due to increased interest in the repo, I've dockerized the app as of this update for quick setup! See setup instructions below. I realize this still takes some time with installing docker dependencies, so it's on the roadmap to have a deployed demo. Besides this, I've made major improvements for a lot of the existing features across the board, mainly UI/UX.
|
||||
|
||||
|
||||
Also worth noting, the method to access the Free Version is no longer working, so I've removed it from model selection until further notice.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-04</strong></summary>
|
||||
Custom prompt prefixing and labeling is now supported through the official API. This nets some interesting results when you need ChatGPT for specific uses or entertainment. Select 'CustomGPT' in the model menu to configure this, and you can choose to save the configuration or reference it by conversation. Model selection will change by conversation.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-03-01</strong></summary>
|
||||
Official ChatGPT API is out! Removed davinci since the official API is extremely fast and 10x less expensive. Since user labeling and prompt prefixing is officially supported, I will add a View feature so you can set this within chat, which gives the UI an added use case. I've kept the BrowserClient, since it's free to use like the official site.
|
||||
|
||||
The Messages UI correctly mirrors code syntax highlighting. The exact replication of the cursor is not 1-to-1 yet, but pretty close. Later on in the project, I'll implement tests for code edge cases and explore the possibility of running code in-browser. Right now, unknown code defaults to javascript, but will detect language as close as possible.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-02-21</strong></summary>
|
||||
BingAI is integrated (although sadly limited by Microsoft with the 5 msg/convo limit, 50 msgs/day). I will need to handle the case when Bing refuses to give more answers on top of the other styling features I have in mind. Official ChatGPT use is back with the new BrowserClient. Brainstorming how to handle the UI when the Ai model changes, since conversations can't be persisted between them (or perhaps build a way to achieve this at some level).
|
||||
</details>
|
||||
<details >
|
||||
<summary><strong>2023-02-15</strong></summary>
|
||||
Just got access to Bing AI so I'll be focusing on integrating that through waylaidwanderer's 'experimental' BingAIClient.
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-02-14</strong></summary>
|
||||
|
||||
Official ChatGPT use is no longer possible though I recently used it with waylaidwanderer's [reverse proxy method](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#using-a-reverse-proxy), and before that, through leaked models he also discovered.
|
||||
|
||||
Currently, this project is only functional with the `text-davinci-003` model.
|
||||
</details>
|
||||
</details>
|
||||
|
||||
# Table of Contents
|
||||
- [ChatGPT Clone](#chatgpt-clone)
|
||||
- [All AI Conversations under One Roof.](#all-ai-conversations-under-one-roof)
|
||||
- [Updates](#updates)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Features](#features)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Usage](#usage)
|
||||
- [Local](#local)
|
||||
- [Docker](#docker)
|
||||
- [Access Tokens](#access-tokens)
|
||||
- [Proxy](#proxy)
|
||||
- [User System](#user-system)
|
||||
- [Updating](#updating)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Origin](#origin)
|
||||
- [Caveats](#caveats)
|
||||
- [Regarding use of Official ChatGPT API](#regarding-use-of-official-chatgpt-api)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Roadmap
|
||||
|
||||
> **Warning**
|
||||
|
||||
> This is a work in progress. I'm building this in public. FYI there is still a lot of tech debt to cleanup. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>Here are my recently completed and planned features:</strong></summary>
|
||||
|
||||
- [x] Persistent conversation
|
||||
- [x] Rename, delete conversations
|
||||
- [x] UI Error handling
|
||||
- [x] Bing AI integration
|
||||
- [x] AI model change handling (start new convos within existing, remembers last selected)
|
||||
- [x] Code block handling (highlighting, markdown, clipboard, language detection)
|
||||
- [x] Markdown handling
|
||||
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
|
||||
- [x] Server convo pagination (limit fetch and load more with 'show more' button)
|
||||
- [x] Config file for easy startup (docker compose)
|
||||
- [x] Mobile styling (thanks to [wtlyu](https://github.com/wtlyu))
|
||||
- [x] Resubmit/edit sent messages (thanks to [wtlyu](https://github.com/wtlyu))
|
||||
- [ ] Message Search
|
||||
- [ ] Custom params for ChatGPT API (temp, top_p, presence_penalty)
|
||||
- [ ] Bing AI Styling (params, suggested responses, convo end, etc.) - **In progress**
|
||||
- [ ] Add warning before clearing convos
|
||||
- [ ] Build test suite for CI/CD
|
||||
- [ ] Prompt Templates/Search
|
||||
- [ ] Refactor/clean up code (tech debt)
|
||||
- [ ] Optional use of local storage for credentials
|
||||
- [ ] Deploy demo
|
||||
|
||||
</details>
|
||||
|
||||
### Features
|
||||
|
||||
- Response streaming identical to ChatGPT through server-sent events
|
||||
- UI from original ChatGPT, including Dark mode
|
||||
- AI model selection (official ChatGPT API, BingAI, ChatGPT Free)
|
||||
- Create and Save custom ChatGPTs*
|
||||
- Edit and Resubmit messages just like the official site (with conversation branching)
|
||||
- Search all messages/conversations - [see details here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)
|
||||
|
||||
^* ChatGPT can be 'customized' by setting a system message or prompt prefix and alternate 'role' to the API request^
|
||||
|
||||
[More info here](https://platform.openai.com/docs/guides/chat/instructing-chat-models). Here's an [example from this app.]()
|
||||
|
||||
### Tech Stack
|
||||
|
||||
##### Common across web and mobile
|
||||
|
||||
- [Frappe Framework](https://frappeframework.com) - an open-source full stack development framework. It uses Python, MariaDB/Postgres, socket.io and Redis
|
||||
- [React](https://react.dev)
|
||||
- [Frappe React SDK](https://github.com/The-Commit-Company/frappe-react-sdk) - React Hooks library that we created/maintain to handle auth, data fetching and API calls to the Frappe Framework backend
|
||||
- [Tailwind CSS](https://tailwindcss.com/) -
|
||||
- [Firebase](https://firebase.google.com) - Push notifications, Crash Analytics and Performance Monitoring
|
||||
|
||||
##### Web
|
||||
|
||||
- [RadixUI](https://www.radix-ui.com) - Accessible UI components for the web
|
||||
- [TipTap](https://www.tiptap.dev) - Rich Text Editor
|
||||
- [React Icons](https://react-icons.github.io/react-icons/) - icon library
|
||||
- [emoji-mart](https://github.com/missive/emoji-mart) - Emoji Picker library
|
||||
|
||||
##### Mobile
|
||||
- [React Native](https://reactnative.dev)
|
||||
- [Expo](https://expo.dev) - for all plugins/utilities and deployment
|
||||
- [NativeWind](https://www.nativewind.dev) - using TailwindCSS but on React Native
|
||||
- [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/) - Animations for React Native
|
||||
- [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler) - to handle touch and gestures on React Native
|
||||
- [Zeego](https://zeego.dev) - Dropdown menus for React Native
|
||||
<details>
|
||||
<summary><strong>This project uses:</strong></summary>
|
||||
|
||||
|
||||
|
||||
<br>
|
||||
- [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
|
||||
- No React boilerplate/toolchain/clone tutorials, created from scratch with react@latest
|
||||
- Use of Tailwind CSS and [shadcn/ui](https://github.com/shadcn/ui) components
|
||||
- Docker, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
|
||||
</details>
|
||||
|
||||
## Production setup
|
||||
|
||||
### Managed Hosting
|
||||
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
|
||||
|
||||
It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
|
||||
## Getting Started
|
||||
|
||||
<div>
|
||||
<a href="https://frappecloud.com/dashboard/signup?product=raven" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
|
||||
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
### Prerequisites
|
||||
- npm
|
||||
- Node.js >= 19.0.0
|
||||
- MongoDB installed or [MongoDB Atlas](https://account.mongodb.com/account/login) (required if not using Docker)
|
||||
- MongoDB does not support older ARM CPUs like those found in Raspberry Pis. However, you can make it work by setting MongoDB's version to mongo:4.4.18 in docker-compose.yml, the most recent version compatible with
|
||||
- [Docker (optional)](https://www.docker.com/get-started/)
|
||||
- [OpenAI API key](https://platform.openai.com/account/api-keys)
|
||||
- BingAI, ChatGPT access tokens (optional, free AIs)
|
||||
|
||||
### Self Hosting
|
||||
|
||||
Follow these steps to set up Raven in production:
|
||||
|
||||
**Step 1**: Download the easy install script
|
||||
## Usage
|
||||
|
||||
- **Clone/download** the repo down where desired
|
||||
```bash
|
||||
wget https://frappe.io/easy-install.py
|
||||
git clone https://github.com/danny-avila/chatgpt-clone.git
|
||||
```
|
||||
- If using MongoDB Atlas, remove `&w=majority` from default connection string.
|
||||
|
||||
**Step 2**: Run the deployment command
|
||||
|
||||
```bash
|
||||
python3 ./easy-install.py deploy \
|
||||
--project=raven_prod_setup \
|
||||
--email=your_email.example.com \
|
||||
--image=ghcr.io/the-commit-company/raven \
|
||||
--version=stable \
|
||||
--app=raven \
|
||||
--sitename subdomain.domain.tld
|
||||
```
|
||||
|
||||
Replace the following parameters with your values:
|
||||
- `your_email.example.com`: Your email address
|
||||
- `subdomain.domain.tld`: Your domain name where Raven will be hosted
|
||||
|
||||
The script will set up a production-ready instance of Raven with all the necessary configurations in about 5 minutes.
|
||||
|
||||
|
||||
## Development Setup
|
||||
### Local
|
||||
### **[In-depth instructions here!](https://github.com/danny-avila/chatgpt-clone/blob/0d4f0f74c04337aaf51b9a3eef898165a7009156/LOCAL_INSTALL.md)**
|
||||
- thank you [@fuegovic](https://github.com/fuegovic)!
|
||||
|
||||
### Docker
|
||||
|
||||
You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, follow below steps:
|
||||
- **Provide** all credentials, (API keys, access tokens, and Mongo Connection String) in [docker-compose.yml](docker-compose.yml) under api service
|
||||
- **Run** `docker-compose up` to start the app
|
||||
- Note: MongoDB does not support older ARM CPUs like those found in Raspberry Pis. However, you can make it work by setting MongoDB's version to mongo:4.4.18 in docker-compose.yml, the most recent version compatible with
|
||||
|
||||
**Step 1**: Setup folder and download the required files
|
||||
### Access Tokens
|
||||
|
||||
mkdir raven
|
||||
cd raven
|
||||
<details>
|
||||
<summary><strong>ChatGPT Free Instructions</strong></summary>
|
||||
|
||||
# Download the docker-compose file
|
||||
wget -O docker-compose.yml https://raw.githubusercontent.com/The-Commit-Company/raven/develop/docker/docker-compose.yml
|
||||
|
||||
# Download the setup script
|
||||
wget -O init.sh https://raw.githubusercontent.com/The-Commit-Company/raven/develop/docker/init.sh
|
||||
|
||||
**Step 2**: Run the container and daemonize it
|
||||
|
||||
docker compose up -d
|
||||
|
||||
**Step 3**: The site [http://raven.localhost:8000/raven](http://raven.localhost:8000/raven) should now be available. The default credentials are:
|
||||
- Username: Administrator
|
||||
- Password: admin
|
||||
|
||||
### Local
|
||||
|
||||
To setup the repository locally follow the steps mentioned below:
|
||||
|
||||
1. Install bench and setup a `frappe-bench` directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation)
|
||||
2. Start the server by running
|
||||
```bash
|
||||
bench start
|
||||
```
|
||||
3. In a separate terminal window, create a new site by running
|
||||
```bash
|
||||
bench new-site raven.test
|
||||
```
|
||||
4. Map your site to localhost with the command
|
||||
```bash
|
||||
bench --site raven.test add-to-hosts
|
||||
```
|
||||
5. Download the Raven app. Run
|
||||
```bash
|
||||
bench get-app https://github.com/The-Commit-Company/raven
|
||||
```
|
||||
6. Install the app on the site
|
||||
```bash
|
||||
bench --site raven.test install-app raven
|
||||
```
|
||||
7. Now open the URL `http://raven.test:8000/raven` in your browser, you should see the app running
|
||||
To get your Access token For ChatGPT 'Free Version', login to chat.openai.com, then visit https://chat.openai.com/api/auth/session.
|
||||
|
||||
|
||||
###### Other Local Configurations
|
||||
**Warning:** There may be a high chance of your account being banned with this method. Continue doing so at your own risk.
|
||||
|
||||
Make sure that you have enabled [developer mode](https://frappeframework.com/how-to-enable-developer-mode-in-frappe) in your Frappe site config. You can do so by running the following in your bench directory:
|
||||
</details>
|
||||
|
||||
```bash
|
||||
bench set-config -g developer_mode 1
|
||||
```
|
||||
<details>
|
||||
<summary><strong>BingAI Instructions</strong></summary>
|
||||
The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while logged into the site to view it.
|
||||
|
||||
You also need to disable CSRF (add `ignore_csrf: 1` in your `site_config.json`) since the React web server will not have any CSRF token in live reload mode. Please note that this is only for the local dev setup and is **not meant for Production**.
|
||||
**Note:** Specific error handling and styling for this model is still in progress.
|
||||
</details>
|
||||
|
||||
```bash
|
||||
bench --site raven.test set-config ignore_csrf 1
|
||||
```
|
||||
### Proxy
|
||||
|
||||
If your server cannot connect to the chatGPT API server by some reason, (eg in China). You can set a environment variable `PROXY`. This will be transmitted to `node-chatgpt-api` interface.
|
||||
|
||||
##### Web App
|
||||
**Warning:** `PROXY` is not `reverseProxyUrl` in `node-chatgpt-api`
|
||||
|
||||
Raven's **web app** is a React based single page app located in the `/frontend` directory. It uses Vite to compile the app.
|
||||
If you want to make changes to the frontend with hot reloading, follow the steps below:
|
||||
<details>
|
||||
<summary><strong>Set up proxy in local environment </strong></summary>
|
||||
|
||||
1. Navigate to the apps/raven folder in your bench directory
|
||||
```bash
|
||||
cd apps/raven
|
||||
```
|
||||
2. Start the local web server
|
||||
```bash
|
||||
yarn run dev
|
||||
```
|
||||
Here is two ways to set proxy.
|
||||
- Option 1: system level environment
|
||||
`export PROXY="http://127.0.0.1:7890"`
|
||||
- Option 2: set in .env file
|
||||
`PROXY="http://127.0.0.1:7890"`
|
||||
|
||||
This will start a local server at `http://localhost:8080` where the frontend app can be previewed in real time as you make changes. To make sure the setup works correctly, add a `.env.local` file in the `/frontend` directory with the following contents:
|
||||
**Change `http://127.0.0.1:7890` to your proxy server**
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Set up proxy in docker environment </strong></summary>
|
||||
|
||||
set in docker-compose.yml file, under services - api - environment
|
||||
|
||||
```
|
||||
VITE_BASE_NAME='' # This needs to be set as empty
|
||||
VITE_SOCKET_PORT=9000 # This is the port on which socket.io is running. It's usually the main Frappe server port (8000) but in the 9000 series
|
||||
VITE_SITE_NAME='raven.test' # This is the same of the site
|
||||
api:
|
||||
...
|
||||
environment:
|
||||
...
|
||||
- "PROXY=http://127.0.0.1:7890"
|
||||
# add this line ↑
|
||||
```
|
||||
|
||||
**Change `http://127.0.0.1:7890` to your proxy server**
|
||||
|
||||
##### Mobile App
|
||||
</details>
|
||||
|
||||
Raven's **mobile app** is a React Native based app built using Expo. It requires an [Expo Development Build](https://docs.expo.dev/develop/development-builds/introduction/) and will not run on Expo Go.
|
||||
### User System
|
||||
|
||||
To get started, you need to install the [Expo CLI](https://docs.expo.dev/develop/tools/#expo-cli)
|
||||
By default, there is no user system enabled, so anyone can access your server.
|
||||
|
||||
```bash
|
||||
npm install -g eas-cli
|
||||
**This project is not designed to provide a complete and full-featured user system.** It's not high priority task and might never be provided.
|
||||
|
||||
[wtlyu](https://github.com/wtlyu) provide a sample user system structure, that you can implement your own user system. It's simple and not a ready-for-use edition.
|
||||
|
||||
(If you want to implement your user system, open this ↓)
|
||||
|
||||
<details>
|
||||
<summary><strong>Implement your own user system </strong></summary>
|
||||
|
||||
To enable the user system, set `ENABLE_USER_SYSTEM=1` in your `.env` file.
|
||||
|
||||
The sample structure is simple. It provide three basic endpoint:
|
||||
|
||||
1. `/auth/login` will redirect to your own login url. In the sample code, it's `/auth/your_login_page`.
|
||||
2. `/auth/logout` will redirect to your own logout url. In the sample code, it's `/auth/your_login_page/logout`.
|
||||
3. `/api/me` will return the userinfo: `{ username, display }`.
|
||||
1. `username` will be used in db, used to distinguish between users.
|
||||
2. `display` will be displayed in UI.
|
||||
|
||||
The only one thing that drive user system work is `req.session.user`. Once it's set, the client will be trusted. Set to `null` if logout.
|
||||
|
||||
Please refer to `/api/server/routes/authYourLogin.js` file. It's very clear and simple to tell you how to implement your user system.
|
||||
|
||||
Or you can ask chatGPT to write the code for you, here is one example to connect LDAP:
|
||||
|
||||
```
|
||||
Please write me an express module, that serve the login and logout endpoint as a router. The login and logout uri is '/' and '/logout'. Once loginned, save display name and username in session.user, as {display, username}. Then redirect to '/'. Please write the code using express and other lib, and storage any server configuration in a config variable. I want the user to be connected to my LDAP server.
|
||||
```
|
||||
|
||||
You also need to have Xcode and/or Android Studio installed locally depending on your platform of choice.
|
||||
|
||||
The mobile app is located under the `apps/mobile` directory.
|
||||
To start the development server and run it on a simulator, you can run
|
||||
```bash
|
||||
yarn run ios
|
||||
```
|
||||
|
||||
or
|
||||
```
|
||||
yarn run android
|
||||
```
|
||||
|
||||
This *should* start the emulators with the app running. For more info, please refer to the [Expo documentation](https://docs.expo.dev/get-started/introduction/)
|
||||
</details>
|
||||
|
||||
|
||||
## Forum and Support
|
||||
### Updating
|
||||
|
||||
- [Community Forum & Support](https://community.ravenapp.cloud)
|
||||
- [YouTube](https://www.youtube.com/@TheCommitCompany)
|
||||
<hr>
|
||||
- As the project is still a work-in-progress, you should pull the latest and run the steps over. Reset your browser cache/clear site data.
|
||||
|
||||
## Repo Activity
|
||||
|
||||

|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/The-Commit-Company/raven/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=The-Commit-Company/raven" />
|
||||
</a>
|
||||
## Use Cases ##
|
||||
|
||||
|
||||
## Reporting Bugs
|
||||
If you find any bugs, feel free to report them here on [GitHub Issues](https://github.com/The-Commit-Company/raven/issues). Make sure you share enough information (app screenshots, browser console screenshots, stack traces, etc) for project maintainers to replicate your bug.
|
||||
|
||||
<details>
|
||||
<summary><strong> Why use this project? </strong></summary>
|
||||
|
||||
- One stop shop for all conversational AIs, with the added bonus of searching past conversations.
|
||||
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
|
||||
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
|
||||
cannot be searched past a certain timeframe.
|
||||
- **Customize ChatGPT**
|
||||
|
||||

|
||||
|
||||
- **API is not as limited as ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat))**
|
||||
|
||||

|
||||
|
||||
- **ChatGPT Free is down.**
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<hr>
|
||||
## Origin ##
|
||||
This project was started early in Feb '23, anticipating the release of the official ChatGPT API from OpenAI, which is now used. It was originally created as a Minimum Viable Product (or MVP) for the [@HackReactor](https://github.com/hackreactor/) Bootcamp. It was built with OpenAI response streaming and most of the UI completed in under 20 hours. During the end of that time, I had most of the UI and basic functionality done. This was created without using any boilerplates or templates, including create-react-app and other toolchains. I didn't follow any 'un-official chatgpt' video tutorials, and simply referenced the official site for the UI. The purpose of the exercise was to learn setting up a full stack project from scratch. Please feel free to give feedback, suggestions, or fork the project for your own use.
|
||||
|
||||
|
||||
## Caveats
|
||||
### Regarding use of Official ChatGPT API
|
||||
From [@waylaidwanderer](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#caveats):
|
||||
|
||||
Since `gpt-3.5-turbo` is ChatGPT's underlying model, I had to do my best to replicate the way the official ChatGPT website uses it.
|
||||
This means my implementation or the underlying model may not behave exactly the same in some ways:
|
||||
- Conversations are not tied to any user IDs, so if that's important to you, you should implement your own user ID system.
|
||||
- ChatGPT's model parameters (temperature, frequency penalty, etc.) are unknown, so I set some defaults that I thought would be reasonable.
|
||||
- Conversations are limited to roughly the last 3000 tokens, so earlier messages may be forgotten during longer conversations.
|
||||
- This works in a similar way to ChatGPT, except I'm pretty sure they have some additional way of retrieving context from earlier messages when needed (which can probably be achieved with embeddings, but I consider that out-of-scope for now).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions and suggestions welcome! Bug reports and fixes are welcome!
|
||||
|
||||
For new features, components, or extensions, please open an issue and discuss before sending a PR.
|
||||
|
||||
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
|
||||
|
||||
## License
|
||||
|
||||
AGPLv3
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Server configuration.
|
||||
# The server will listen to localhost:3080 request by default. You can set the target ip as you want.
|
||||
# If you want this server can be used outside your local machine, for example to share with other
|
||||
# machine or expose this from a docker container, set HOST=0.0.0.0 or your external ip interface.
|
||||
#
|
||||
# Tips: HOST=0.0.0.0 means listening on all interface. It's not a real ip. Use localhost:port rather
|
||||
# than 0.0.0.0:port to open it.
|
||||
HOST=localhost
|
||||
PORT=3080
|
||||
NODE_ENV=development
|
||||
|
||||
# Change this to proxy any API request. It's useful if your machine have difficulty calling the original API server.
|
||||
# PROXY="http://YOUR_PROXY_SERVER"
|
||||
|
||||
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
|
||||
MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
||||
|
||||
# API key configuration.
|
||||
# Leave blank if you don't want them.
|
||||
OPENAI_KEY=
|
||||
|
||||
# Default ChatGPT API Model, options: 'gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'
|
||||
# you will have errors if you don't have access to a model like 'gpt-4', defaults to turbo if left empty/excluded.
|
||||
DEFAULT_API_GPT=gpt-3.5-turbo
|
||||
|
||||
# _U Cookies Value from bing.com
|
||||
BING_TOKEN=
|
||||
|
||||
# ChatGPT Browser Client (free but use at your own risk)
|
||||
# Access token from https://chat.openai.com/api/auth/session
|
||||
# Exposes your access token to a 3rd party
|
||||
CHATGPT_TOKEN=
|
||||
# If you have access to other models on the official site, you can use them here.
|
||||
# Defaults to 'text-davinci-002-render-sha' if left empty.
|
||||
# options: gpt-4, text-davinci-002-render, text-davinci-002-render-paid, or text-davinci-002-render-sha
|
||||
# You cannot use a model that your account does not have access to. You can check
|
||||
# which ones you have access to by opening DevTools and going to the Network tab.
|
||||
# Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
BROWSER_MODEL=
|
||||
|
||||
# ENABLING SEARCH MESSAGES/CONVOS
|
||||
# Requires installation of free self-hosted Meilisearch or Paid Remote Plan (Remote not tested)
|
||||
# The easiest setup for this is through docker-compose, which takes care of it for you.
|
||||
# SEARCH=TRUE
|
||||
SEARCH=TRUE
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for api server to connect to the search server.
|
||||
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||
# MEILI_HOST='http://meilisearch:7700' # <-- docker-compose (should already be setup on docker-compose.yml)
|
||||
MEILI_HOST='http://0.0.0.0:7700' # <-- local/remote
|
||||
|
||||
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
|
||||
# must replace '0.0.0.0' with 'meilisearch' if serving meilisearch with docker-compose
|
||||
# MEILI_HTTP_ADDR='meilisearch:7700' # <-- docker-compose (should already be setup on docker-compose.yml)
|
||||
MEILI_HTTP_ADDR='0.0.0.0:7700' # <-- local/remote
|
||||
|
||||
# REQUIRED FOR SEARCH: In production env., needs a secure key, feel free to generate your own.
|
||||
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
|
||||
# Meilisearch will throw an error and refuse to launch if no master key is provided or if it is under 16 bytes,
|
||||
# Meilisearch will suggest a secure autogenerated master key.
|
||||
# Using docker, it seems recognized as production so use a secure key.
|
||||
# MEILI_MASTER_KEY= # <-- empty/insecure key works for local/remote
|
||||
MEILI_MASTER_KEY=JKMW-hGc7v_D1FkJVdbRSDNFLZcUv3S75yrxXP0SmcU # <-- ready made secure key for docker-compose
|
||||
|
||||
|
||||
# User System
|
||||
# global enable/disable the sample user system.
|
||||
# this is not a ready to use user system.
|
||||
# dont't use it, unless you can write your own code.
|
||||
# ENABLE_USER_SYSTEM= # <-- make sure you don't comment this back in if you're not using your own user system
|
||||
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
extends: ['eslint:recommended'],
|
||||
overrides: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
'arrow-parens': [2, 'as-needed', { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'import/extensions': 'off',
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
functions: false
|
||||
}
|
||||
],
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-restricted-syntax': 'off'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 110,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askBing = async ({ text, onProgress, convo }) => {
|
||||
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
|
||||
const bingAIClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BING_TOKEN,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
debug: false,
|
||||
cache: { store: new KeyvFile({ filename: './data/cache.json' }) },
|
||||
proxy: process.env.PROXY || null
|
||||
});
|
||||
|
||||
let options = { onProgress };
|
||||
if (convo) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
if (options?.jailbreakConversationId == 'false') {
|
||||
options.jailbreakConversationId = false;
|
||||
}
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
}
|
||||
|
||||
console.log('bing options', options);
|
||||
|
||||
const res = await bingAIClient.sendMessage(text, options);
|
||||
|
||||
return res;
|
||||
|
||||
// Example response for reference
|
||||
// {
|
||||
// conversationSignature: 'wwZ2GC/qRgEqP3VSNIhbPGwtno5RcuBhzZFASOM+Sxg=',
|
||||
// conversationId: '51D|BingProd|026D3A4017554DE6C446798144B6337F4D47D5B76E62A31F31D0B1D0A95ED868',
|
||||
// clientId: '914800201536527',
|
||||
// invocationId: 1,
|
||||
// conversationExpiryTime: '2023-02-15T21:48:46.2892088Z',
|
||||
// response: 'Hello, this is Bing. Nice to meet you. 😊',
|
||||
// details: {
|
||||
// text: 'Hello, this is Bing. Nice to meet you. 😊',
|
||||
// author: 'bot',
|
||||
// createdAt: '2023-02-15T15:48:43.0631898+00:00',
|
||||
// timestamp: '2023-02-15T15:48:43.0631898+00:00',
|
||||
// messageId: '9d0c9a80-91b1-49ab-b9b1-b457dc3fe247',
|
||||
// requestId: '5b252ef8-4f09-4c08-b6f5-4499d2e12fba',
|
||||
// offense: 'None',
|
||||
// adaptiveCards: [ [Object] ],
|
||||
// sourceAttributions: [],
|
||||
// feedback: { tag: null, updatedOn: null, type: 'None' },
|
||||
// contentOrigin: 'DeepLeo',
|
||||
// privacy: null,
|
||||
// suggestedResponses: [ [Object], [Object], [Object] ]
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
module.exports = { askBing };
|
||||
@@ -0,0 +1,45 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(["gpt-4", "text-davinci-002-render", "text-davinci-002-render-paid", "text-davinci-002-render-sha"]);
|
||||
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
reverseProxyUrl: 'https://bypass.duti.tech/api/conversation',
|
||||
// Access token from https://chat.openai.com/api/auth/session
|
||||
accessToken: process.env.CHATGPT_TOKEN,
|
||||
// debug: true
|
||||
proxy: process.env.PROXY || null,
|
||||
};
|
||||
|
||||
// You can check which models you have access to by opening DevTools and going to the Network tab.
|
||||
// Refresh the page and look at the response body for https://chat.openai.com/backend-api/models.
|
||||
if (set.has(process.env.BROWSER_MODEL)) {
|
||||
clientOptions.model = process.env.BROWSER_MODEL;
|
||||
}
|
||||
|
||||
const browserClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const client = new ChatGPTBrowserClient(clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
console.log('gptBrowser options', options, clientOptions);
|
||||
|
||||
/* will error if given a convoId at the start */
|
||||
if (convo.parentMessageId.startsWith('0000')) {
|
||||
delete options.conversationId;
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = { browserClient };
|
||||
@@ -0,0 +1,34 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const set = new Set(['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301']);
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
if (set.has(process.env.DEFAULT_API_GPT)) {
|
||||
clientOptions.modelOptions.model = process.env.DEFAULT_API_GPT;
|
||||
}
|
||||
|
||||
const askClient = async ({ text, onProgress, convo, abortController }) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
let options = { onProgress, abortController };
|
||||
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = { askClient };
|
||||
@@ -0,0 +1,35 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const clientOptions = {
|
||||
modelOptions: {
|
||||
model: 'gpt-3.5-turbo'
|
||||
},
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
};
|
||||
|
||||
const customClient = async ({ text, onProgress, convo, promptPrefix, chatGptLabel, abortController }) => {
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
clientOptions.chatGptLabel = chatGptLabel;
|
||||
|
||||
if (promptPrefix?.length > 0) {
|
||||
clientOptions.promptPrefix = promptPrefix;
|
||||
}
|
||||
|
||||
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
|
||||
|
||||
let options = { onProgress, abortController };
|
||||
if (!!convo.parentMessageId && !!convo.conversationId) {
|
||||
options = { ...options, ...convo };
|
||||
}
|
||||
|
||||
const res = await client.sendMessage(text, options);
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = customClient;
|
||||
@@ -0,0 +1,40 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
|
||||
const askSydney = async ({ text, onProgress, convo }) => {
|
||||
const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api'));
|
||||
|
||||
const sydneyClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BING_TOKEN,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
debug: false,
|
||||
cache: { store: new KeyvFile({ filename: './data/cache.json' }) }
|
||||
});
|
||||
|
||||
let options = {
|
||||
jailbreakConversationId: true,
|
||||
onProgress,
|
||||
};
|
||||
|
||||
if (convo.jailbreakConversationId) {
|
||||
options = { ...options, jailbreakConversationId: convo.jailbreakConversationId, parentMessageId: convo.parentMessageId };
|
||||
}
|
||||
|
||||
if (convo.toneStyle) {
|
||||
options.toneStyle = convo.toneStyle;
|
||||
}
|
||||
|
||||
console.log('sydney options', options);
|
||||
|
||||
const res = await sydneyClient.sendMessage(text, options
|
||||
);
|
||||
|
||||
return res;
|
||||
|
||||
// for reference:
|
||||
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
|
||||
};
|
||||
|
||||
module.exports = { askSydney };
|
||||
@@ -0,0 +1,19 @@
|
||||
const { askClient } = require('./clients/chatgpt-client');
|
||||
const { browserClient } = require('./clients/chatgpt-browser');
|
||||
const { askBing } = require('./clients/bingai');
|
||||
const { askSydney } = require('./clients/sydney');
|
||||
const customClient = require('./clients/chatgpt-custom');
|
||||
const titleConvo = require('./titleConvo');
|
||||
const getCitations = require('../lib/parse/getCitations');
|
||||
const citeText = require('../lib/parse/citeText');
|
||||
|
||||
module.exports = {
|
||||
askClient,
|
||||
browserClient,
|
||||
customClient,
|
||||
askBing,
|
||||
askSydney,
|
||||
titleConvo,
|
||||
getCitations,
|
||||
citeText,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const _ = require('lodash');
|
||||
|
||||
const proxyEnvToAxiosProxy = proxyString => {
|
||||
if (!proxyString) return null;
|
||||
|
||||
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||
const [, protocol, username, password, host, port] = proxyString.match(regex);
|
||||
const proxyConfig = {
|
||||
protocol,
|
||||
host,
|
||||
port: port ? parseInt(port) : undefined,
|
||||
auth: username && password ? { username, password } : undefined
|
||||
};
|
||||
|
||||
return proxyConfig;
|
||||
};
|
||||
|
||||
const titleConvo = async ({ model, text, response }) => {
|
||||
let title = 'New Chat';
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
// `You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user, using the same language. The requirement are: 1. If possible, generate in 5 words or less, 2. Using title case, 3. must give the title using the language as the user said. 4. Don't refer to the participants of the conversation. 5. Do not include punctuation or quotation marks. 6. Your response should be in title case, exclusively containing the title. 7. don't say anything except the title.
|
||||
`Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect. Write in the detected language. Title in 5 Words or Less. No Punctuation/Quotation. All first letters of every word should be capitalized and complete only the title in User Language only.
|
||||
|
||||
||>User:
|
||||
"${text}"
|
||||
||>Response:
|
||||
"${JSON.stringify(response?.text)}"
|
||||
|
||||
||>Title:`
|
||||
}
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: `User:\n "${text}"\n\n${model}: \n"${JSON.stringify(response?.text)}"\n\n`
|
||||
// }
|
||||
];
|
||||
|
||||
// console.log('Title Prompt', messages[0]);
|
||||
|
||||
const request = {
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages,
|
||||
temperature: 0,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0
|
||||
};
|
||||
|
||||
// console.log('REQUEST', request);
|
||||
|
||||
try {
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPENAI_KEY
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
const completion = await openai.createChatCompletion(request, {
|
||||
proxy: proxyEnvToAxiosProxy(process.env.PROXY || null)
|
||||
});
|
||||
|
||||
//eslint-disable-next-line
|
||||
title = completion.data.choices[0].message.content.replace(/["\.]/g, '');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log('There was an issue generating title, see error above');
|
||||
}
|
||||
|
||||
console.log('CONVERSATION TITLE', title);
|
||||
return title;
|
||||
};
|
||||
|
||||
const throttledTitleConvo = _.throttle(titleConvo, 1000);
|
||||
|
||||
module.exports = throttledTitleConvo;
|
||||
@@ -0,0 +1,44 @@
|
||||
require('dotenv').config();
|
||||
const mongoose = require('mongoose');
|
||||
const MONGO_URI = process.env.MONGO_URI;
|
||||
|
||||
if (!MONGO_URI) {
|
||||
throw new Error('Please define the MONGO_URI environment variable inside .env.local');
|
||||
}
|
||||
|
||||
/**
|
||||
* Global is used here to maintain a cached connection across hot reloads
|
||||
* in development. This prevents connections growing exponentially
|
||||
* during API Route usage.
|
||||
*/
|
||||
let cached = global.mongoose;
|
||||
|
||||
if (!cached) {
|
||||
cached = global.mongoose = { conn: null, promise: null };
|
||||
}
|
||||
|
||||
async function connectDb() {
|
||||
if (cached.conn) {
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
const opts = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
bufferCommands: false
|
||||
// bufferMaxEntries: 0,
|
||||
// useFindAndModify: true,
|
||||
// useCreateIndex: true
|
||||
};
|
||||
|
||||
mongoose.set('strictQuery', true);
|
||||
cached.promise = mongoose.connect(MONGO_URI, opts).then((mongoose) => {
|
||||
return mongoose;
|
||||
});
|
||||
}
|
||||
cached.conn = await cached.promise;
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
module.exports = connectDb;
|
||||
@@ -0,0 +1,70 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Conversation = mongoose.models.Conversation;
|
||||
const Message = mongoose.models.Message;
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
let currentTimeout = null;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function indexSync(req, res, next) {
|
||||
try {
|
||||
if (!process.env.MEILI_HOST || !process.env.MEILI_MASTER_KEY || !process.env.SEARCH) {
|
||||
throw new Error('Meilisearch not configured, search will be disabled.');
|
||||
}
|
||||
|
||||
const client = new MeiliSearch({
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY
|
||||
});
|
||||
|
||||
const { status } = await client.health();
|
||||
// console.log(`Meilisearch: ${status}`);
|
||||
const result = status === 'available' && !!process.env.SEARCH;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Meilisearch not available');
|
||||
}
|
||||
|
||||
const messageCount = await Message.countDocuments();
|
||||
const convoCount = await Conversation.countDocuments();
|
||||
const messages = await client.index('messages').getStats();
|
||||
const convos = await client.index('convos').getStats();
|
||||
const messagesIndexed = messages.numberOfDocuments;
|
||||
const convosIndexed = convos.numberOfDocuments;
|
||||
|
||||
console.log(`There are ${messageCount} messages in the database, ${messagesIndexed} indexed`);
|
||||
console.log(`There are ${convoCount} convos in the database, ${convosIndexed} indexed`);
|
||||
|
||||
if (messageCount !== messagesIndexed) {
|
||||
console.log('Messages out of sync, indexing');
|
||||
await Message.syncWithMeili();
|
||||
}
|
||||
|
||||
if (convoCount !== convosIndexed) {
|
||||
console.log('Convos out of sync, indexing');
|
||||
await Conversation.syncWithMeili();
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log('in index sync');
|
||||
if (err.message.includes('not found')) {
|
||||
console.log('Creating indices...');
|
||||
currentTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await Message.syncWithMeili();
|
||||
await Conversation.syncWithMeili();
|
||||
} catch (err) {
|
||||
console.error('Trouble creating indices, try restarting the server.');
|
||||
}
|
||||
}, 750);
|
||||
} else {
|
||||
console.error(err);
|
||||
// res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
console.log('Clearing sync timeouts before exiting...');
|
||||
clearTimeout(currentTimeout);
|
||||
});
|
||||
|
||||
module.exports = indexSync;
|
||||
@@ -0,0 +1,63 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { Conversation, } = require('../../models/Conversation');
|
||||
const { getMessages, } = require('../../models/');
|
||||
|
||||
async function migrateDb() {
|
||||
try {
|
||||
const conversations = await Conversation.find({ model: null }).exec();
|
||||
|
||||
if (!conversations || conversations.length === 0)
|
||||
return { message: '[Migrate] No conversations to migrate' };
|
||||
|
||||
for (let convo of conversations) {
|
||||
const messages = await getMessages({
|
||||
conversationId: convo.conversationId,
|
||||
messageId: { $exists: false }
|
||||
});
|
||||
|
||||
let model;
|
||||
let oldId;
|
||||
const promises = [];
|
||||
messages.forEach((message, i) => {
|
||||
const msgObj = message.toObject();
|
||||
const newId = msgObj.id;
|
||||
if (i === 0) {
|
||||
message.parentMessageId = '00000000-0000-0000-0000-000000000000';
|
||||
} else {
|
||||
message.parentMessageId = oldId;
|
||||
}
|
||||
|
||||
oldId = newId;
|
||||
message.messageId = newId;
|
||||
if (message.sender.toLowerCase() !== 'user' && !model) {
|
||||
model = message.sender.toLowerCase();
|
||||
}
|
||||
|
||||
if (message.sender.toLowerCase() === 'user') {
|
||||
message.isCreatedByUser = true;
|
||||
}
|
||||
|
||||
promises.push(message.save());
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
await Conversation.findOneAndUpdate(
|
||||
{ conversationId: convo.conversationId },
|
||||
{ model },
|
||||
{ new: true }
|
||||
).exec();
|
||||
}
|
||||
|
||||
try {
|
||||
await mongoose.connection.db.collection('messages').dropIndex('id_1');
|
||||
} catch (error) {
|
||||
console.log("[Migrate] Index doesn't exist or already dropped");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: '[Migrate] Error migrating conversations' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = migrateDb;
|
||||
@@ -0,0 +1,31 @@
|
||||
const citationRegex = /\[\^\d+?\^\]/g;
|
||||
|
||||
const citeText = (res, noLinks = false) => {
|
||||
let result = res.text || res;
|
||||
const citations = Array.from(new Set(result.match(citationRegex)));
|
||||
if (citations?.length === 0) return result;
|
||||
|
||||
if (noLinks) {
|
||||
citations.forEach((citation) => {
|
||||
const digit = citation.match(/\d+?/g)[0];
|
||||
// result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
|
||||
result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let sources = res.details.sourceAttributions;
|
||||
if (sources?.length === 0) return result;
|
||||
sources = sources.map((source) => source.seeMoreUrl);
|
||||
|
||||
citations.forEach((citation) => {
|
||||
const digit = citation.match(/\d+?/g)[0];
|
||||
result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
|
||||
// result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = citeText;
|
||||
@@ -0,0 +1,13 @@
|
||||
// const regex = / \[\d+\..*?\]\(.*?\)/g;
|
||||
const regex = / \[.*?]\(.*?\)/g;
|
||||
|
||||
const getCitations = (res) => {
|
||||
const textBlocks = res.details.adaptiveCards[0].body;
|
||||
if (!textBlocks) return '';
|
||||
let links = textBlocks[textBlocks.length - 1]?.text.match(regex);
|
||||
if (links?.length === 0 || !links) return '';
|
||||
links = links.map((link) => link.trim());
|
||||
return links.join('\n');
|
||||
};
|
||||
|
||||
module.exports = getCitations;
|
||||
@@ -0,0 +1,29 @@
|
||||
function mergeSort(arr, compareFn) {
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const mid = Math.floor(arr.length / 2);
|
||||
const leftArr = arr.slice(0, mid);
|
||||
const rightArr = arr.slice(mid);
|
||||
|
||||
return merge(mergeSort(leftArr, compareFn), mergeSort(rightArr, compareFn), compareFn);
|
||||
}
|
||||
|
||||
function merge(leftArr, rightArr, compareFn) {
|
||||
const result = [];
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
|
||||
if (compareFn(leftArr[leftIndex], rightArr[rightIndex]) < 0) {
|
||||
result.push(leftArr[leftIndex++]);
|
||||
} else {
|
||||
result.push(rightArr[rightIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
return result.concat(leftArr.slice(leftIndex)).concat(rightArr.slice(rightIndex));
|
||||
}
|
||||
|
||||
module.exports = mergeSort;
|
||||
@@ -0,0 +1,15 @@
|
||||
const cleanUpPrimaryKeyValue = (value) => {
|
||||
// For Bing convoId handling
|
||||
return value.replace(/--/g, '|');
|
||||
};
|
||||
|
||||
function replaceSup(text) {
|
||||
if (!text.includes('<sup>')) return text;
|
||||
const replacedText = text.replace(/<sup>/g, '^').replace(/\s+<\/sup>/g, '^');
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cleanUpPrimaryKeyValue,
|
||||
replaceSup
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
const mergeSort = require('./mergeSort');
|
||||
const { cleanUpPrimaryKeyValue } = require('./misc');
|
||||
|
||||
function reduceMessages(hits) {
|
||||
const counts = {};
|
||||
|
||||
for (const hit of hits) {
|
||||
if (!counts[hit.conversationId]) {
|
||||
counts[hit.conversationId] = 1;
|
||||
} else {
|
||||
counts[hit.conversationId]++;
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const [conversationId, count] of Object.entries(counts)) {
|
||||
result.push({
|
||||
conversationId,
|
||||
count
|
||||
});
|
||||
}
|
||||
|
||||
return mergeSort(result, (a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
function reduceHits(hits, titles = []) {
|
||||
const counts = {};
|
||||
const titleMap = {};
|
||||
const convos = [...hits, ...titles];
|
||||
|
||||
for (const convo of convos) {
|
||||
const currentId = cleanUpPrimaryKeyValue(convo.conversationId);
|
||||
if (!counts[currentId]) {
|
||||
counts[currentId] = 1;
|
||||
} else {
|
||||
counts[currentId]++;
|
||||
}
|
||||
|
||||
if (convo.title) {
|
||||
// titleMap[currentId] = convo._formatted.title;
|
||||
titleMap[currentId] = convo.title;
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const [conversationId, count] of Object.entries(counts)) {
|
||||
result.push({
|
||||
conversationId,
|
||||
count,
|
||||
title: titleMap[conversationId] ? titleMap[conversationId] : null
|
||||
});
|
||||
}
|
||||
|
||||
return mergeSort(result, (a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
module.exports = { reduceMessages, reduceHits };
|
||||
@@ -0,0 +1,84 @@
|
||||
const mongoose = require('mongoose');
|
||||
const major = [0, 0];
|
||||
const minor = [0, 0];
|
||||
const patch = [0, 5];
|
||||
|
||||
const configSchema = mongoose.Schema(
|
||||
{
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
validate: {
|
||||
validator: function (tag) {
|
||||
const [part1, part2, part3] = tag.replace('v', '').split('.').map(Number);
|
||||
|
||||
// Check if all parts are numbers
|
||||
if (isNaN(part1) || isNaN(part2) || isNaN(part3)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if all parts are within their respective ranges
|
||||
if (part1 < major[0] || part1 > major[1]) {
|
||||
return false;
|
||||
}
|
||||
if (part2 < minor[0] || part2 > minor[1]) {
|
||||
return false;
|
||||
}
|
||||
if (part3 < patch[0] || part3 > patch[1]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
message: 'Invalid tag value'
|
||||
}
|
||||
},
|
||||
searchEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
usersEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
startupCounts: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
// Instance method
|
||||
configSchema.methods.incrementCount = function () {
|
||||
this.startupCounts += 1;
|
||||
};
|
||||
|
||||
// Static methods
|
||||
configSchema.statics.findByTag = async function (tag) {
|
||||
return await this.findOne({ tag });
|
||||
};
|
||||
|
||||
configSchema.statics.updateByTag = async function (tag, update) {
|
||||
return await this.findOneAndUpdate({ tag }, update, { new: true });
|
||||
};
|
||||
|
||||
const Config = mongoose.models.Config || mongoose.model('Config', configSchema);
|
||||
|
||||
module.exports = {
|
||||
getConfigs: async (filter) => {
|
||||
try {
|
||||
return await Config.find(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { config: 'Error getting configs' };
|
||||
}
|
||||
},
|
||||
deleteConfigs: async (filter) => {
|
||||
try {
|
||||
return await Config.deleteMany(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { config: 'Error deleting configs' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
// const { Conversation } = require('./plugins');
|
||||
const Conversation = require('./schema/convoSchema');
|
||||
const { getMessages, deleteMessages } = require('./Message');
|
||||
|
||||
const getConvo = async (user, conversationId) => {
|
||||
try {
|
||||
return await Conversation.findOne({ user, conversationId }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting single conversation' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Conversation,
|
||||
saveConvo: async (user, { conversationId, newConversationId, title, ...convo }) => {
|
||||
try {
|
||||
const messages = await getMessages({ conversationId });
|
||||
const update = { ...convo, messages };
|
||||
if (title) {
|
||||
update.title = title;
|
||||
update.user = user;
|
||||
}
|
||||
if (newConversationId) {
|
||||
update.conversationId = newConversationId;
|
||||
}
|
||||
if (!update.jailbreakConversationId) {
|
||||
update.jailbreakConversationId = null;
|
||||
}
|
||||
if (update.model !== 'chatgptCustom' && update.chatGptLabel && update.promptPrefix) {
|
||||
console.log('Validation error: resetting chatgptCustom fields', update);
|
||||
update.chatGptLabel = null;
|
||||
update.promptPrefix = null;
|
||||
}
|
||||
|
||||
return await Conversation.findOneAndUpdate(
|
||||
{ conversationId: conversationId, user },
|
||||
{ $set: update },
|
||||
{ new: true, upsert: true }
|
||||
).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
},
|
||||
updateConvo: async (user, { conversationId, oldConvoId, ...update }) => {
|
||||
try {
|
||||
let convoId = conversationId;
|
||||
if (oldConvoId) {
|
||||
convoId = oldConvoId;
|
||||
update.conversationId = conversationId;
|
||||
}
|
||||
|
||||
return await Conversation.findOneAndUpdate({ conversationId: convoId, user }, update, {
|
||||
new: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating conversation' };
|
||||
}
|
||||
},
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 12) => {
|
||||
try {
|
||||
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
const convos = await Conversation.find({ user })
|
||||
.sort({ createdAt: -1, created: -1 })
|
||||
.skip((pageNumber - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.exec();
|
||||
|
||||
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting conversations' };
|
||||
}
|
||||
},
|
||||
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 12) => {
|
||||
try {
|
||||
if (!convoIds || convoIds.length === 0) {
|
||||
return { conversations: [], pages: 1, pageNumber, pageSize };
|
||||
}
|
||||
|
||||
const cache = {};
|
||||
const convoMap = {};
|
||||
const promises = [];
|
||||
// will handle a syncing solution soon
|
||||
const deletedConvoIds = [];
|
||||
|
||||
convoIds.forEach(convo =>
|
||||
promises.push(
|
||||
Conversation.findOne({
|
||||
user,
|
||||
conversationId: convo.conversationId
|
||||
}).exec()
|
||||
)
|
||||
);
|
||||
|
||||
const results = (await Promise.all(promises)).filter((convo, i) => {
|
||||
if (!convo) {
|
||||
deletedConvoIds.push(convoIds[i].conversationId);
|
||||
return false;
|
||||
} else {
|
||||
const page = Math.floor(i / pageSize) + 1;
|
||||
if (!cache[page]) {
|
||||
cache[page] = [];
|
||||
}
|
||||
cache[page].push(convo);
|
||||
convoMap[convo.conversationId] = convo;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// const startIndex = (pageNumber - 1) * pageSize;
|
||||
// const convos = results.slice(startIndex, startIndex + pageSize);
|
||||
const totalPages = Math.ceil(results.length / pageSize);
|
||||
cache.pages = totalPages;
|
||||
cache.pageSize = pageSize;
|
||||
return {
|
||||
cache,
|
||||
conversations: cache[pageNumber] || [],
|
||||
pages: totalPages || 1,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
// will handle a syncing solution soon
|
||||
filter: new Set(deletedConvoIds),
|
||||
convoMap
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error fetching conversations' };
|
||||
}
|
||||
},
|
||||
getConvo,
|
||||
/* chore: this method is not properly error handled */
|
||||
getConvoTitle: async (user, conversationId) => {
|
||||
try {
|
||||
const convo = await getConvo(user, conversationId);
|
||||
/* ChatGPT Browser was triggering error here due to convo being saved later */
|
||||
if (convo && !convo.title) {
|
||||
return null;
|
||||
} else {
|
||||
// TypeError: Cannot read properties of null (reading 'title')
|
||||
return convo?.title || 'New Chat';
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error getting conversation title' };
|
||||
}
|
||||
},
|
||||
deleteConvos: async (user, filter) => {
|
||||
let toRemove = await Conversation.find({...filter, user}).select('conversationId')
|
||||
const ids = toRemove.map(instance => instance.conversationId);
|
||||
let deleteCount = await Conversation.deleteMany({...filter, user}).exec();
|
||||
deleteCount.messages = await deleteMessages({conversationId: {$in: ids}});
|
||||
return deleteCount;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const customGptSchema = mongoose.Schema({
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
}, { timestamps: true });
|
||||
|
||||
const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema);
|
||||
|
||||
const createCustomGpt = async ({ chatGptLabel, promptPrefix, value, user }) => {
|
||||
try {
|
||||
await CustomGpt.create({
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
value,
|
||||
user
|
||||
});
|
||||
return { chatGptLabel, promptPrefix, value };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error saving customGpt' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.find({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error getting customGpts' };
|
||||
}
|
||||
},
|
||||
updateCustomGpt: async (user, { value, ...update }) => {
|
||||
try {
|
||||
const customGpt = await CustomGpt.findOne({ value, user }).exec();
|
||||
|
||||
if (!customGpt) {
|
||||
return await createCustomGpt({ value, ...update, user });
|
||||
} else {
|
||||
return await CustomGpt.findOneAndUpdate({ value, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
updateByLabel: async (user, { prevLabel, ...update }) => {
|
||||
try {
|
||||
return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel, user }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
deleteCustomGpts: async (user, filter) => {
|
||||
try {
|
||||
return await CustomGpt.deleteMany({ ...filter, user }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error deleting customGpts' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
const Message = require('./schema/messageSchema');
|
||||
module.exports = {
|
||||
Message,
|
||||
saveMessage: async ({ messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId }, {
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error saving message' };
|
||||
}
|
||||
},
|
||||
saveBingMessage: async ({ messageId, oldMessageId = messageId, conversationId, parentMessageId, sender, text, isCreatedByUser=false, error }) => {
|
||||
try {
|
||||
await Message.findOneAndUpdate({ messageId: oldMessageId }, {
|
||||
messageId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
sender,
|
||||
text,
|
||||
isCreatedByUser,
|
||||
error
|
||||
}, { upsert: true, new: true });
|
||||
return { messageId, conversationId, parentMessageId, sender, text, isCreatedByUser };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error saving message' };
|
||||
}
|
||||
},
|
||||
deleteMessagesSince: async ({ messageId, conversationId }) => {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec()
|
||||
|
||||
if (message)
|
||||
return await Message.find({ conversationId }).deleteMany({ createdAt: { $gt: message.createdAt } }).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
}
|
||||
},
|
||||
getMessages: async (filter) => {
|
||||
try {
|
||||
return await Message.find(filter).sort({createdAt: 1}).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error getting messages' };
|
||||
}
|
||||
},
|
||||
deleteMessages: async (filter) => {
|
||||
try {
|
||||
return await Message.deleteMany(filter).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { message: 'Error deleting messages' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const promptSchema = mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
},
|
||||
}, { timestamps: true });
|
||||
|
||||
const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
|
||||
|
||||
module.exports = {
|
||||
savePrompt: async ({ title, prompt }) => {
|
||||
try {
|
||||
await Prompt.create({
|
||||
title,
|
||||
prompt
|
||||
});
|
||||
return { title, prompt };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error saving prompt' };
|
||||
}
|
||||
},
|
||||
getPrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.find(filter).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error getting prompts' };
|
||||
}
|
||||
},
|
||||
deletePrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.deleteMany(filter).exec()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error deleting prompts' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const { getMessages, saveMessage, saveBingMessage, deleteMessagesSince, deleteMessages } = require('./Message');
|
||||
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt');
|
||||
const { getConvoTitle, getConvo, saveConvo, updateConvo } = require('./Conversation');
|
||||
|
||||
module.exports = {
|
||||
getMessages,
|
||||
saveMessage,
|
||||
saveBingMessage,
|
||||
deleteMessagesSince,
|
||||
deleteMessages,
|
||||
getConvoTitle,
|
||||
getConvo,
|
||||
saveConvo,
|
||||
updateConvo,
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
updateByLabel,
|
||||
deleteCustomGpts
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||
const _ = require('lodash');
|
||||
|
||||
const validateOptions = function (options) {
|
||||
const requiredKeys = ['host', 'apiKey', 'indexName'];
|
||||
requiredKeys.forEach((key) => {
|
||||
if (!options[key]) throw new Error(`Missing mongoMeili Option: ${key}`);
|
||||
});
|
||||
};
|
||||
|
||||
const createMeiliMongooseModel = function ({ index, indexName, client, attributesToIndex }) {
|
||||
// console.log('attributesToIndex', attributesToIndex);
|
||||
const primaryKey = attributesToIndex[0];
|
||||
// MeiliMongooseModel is of type Mongoose.Model
|
||||
class MeiliMongooseModel {
|
||||
// Clear Meili index
|
||||
static async clearMeiliIndex() {
|
||||
await index.delete();
|
||||
// await index.deleteAllDocuments();
|
||||
await this.collection.updateMany(
|
||||
{ _meiliIndex: true },
|
||||
{ $set: { _meiliIndex: false } }
|
||||
);
|
||||
}
|
||||
|
||||
static async resetIndex() {
|
||||
await this.clearMeiliIndex();
|
||||
await client.createIndex(indexName, { primaryKey });
|
||||
}
|
||||
// Clear Meili index
|
||||
// Push a mongoDB collection to Meili index
|
||||
static async syncWithMeili() {
|
||||
await this.resetIndex();
|
||||
// const docs = await this.find();
|
||||
const docs = await this.find({ _meiliIndex: { $in: [null, false] } });
|
||||
console.log('docs', docs.length);
|
||||
await Promise.all(
|
||||
docs.map(function (doc) {
|
||||
return doc.addObjectToMeili();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Set one or more settings of the meili index
|
||||
static async setMeiliIndexSettings(settings) {
|
||||
return await index.updateSettings(settings);
|
||||
}
|
||||
|
||||
// Search the index
|
||||
static async meiliSearch(q, params, populate) {
|
||||
const data = await index.search(q, params);
|
||||
|
||||
// Populate hits with content from mongodb
|
||||
if (populate) {
|
||||
// Find objects into mongodb matching `objectID` from Meili search
|
||||
const query = {};
|
||||
// query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
||||
query[primaryKey] = _.map(data.hits, hit => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
// console.log('query', query);
|
||||
const hitsFromMongoose = await this.find(
|
||||
query,
|
||||
_.reduce(
|
||||
this.schema.obj,
|
||||
function (results, value, key) {
|
||||
return { ...results, [key]: 1 };
|
||||
},
|
||||
{ _id: 1 }
|
||||
),
|
||||
);
|
||||
|
||||
// Add additional data from mongodb into Meili search hits
|
||||
const populatedHits = data.hits.map(function (hit) {
|
||||
const query = {};
|
||||
query[primaryKey] = hit[primaryKey];
|
||||
const originalHit = _.find(hitsFromMongoose, query);
|
||||
|
||||
return {
|
||||
...(originalHit ? originalHit.toJSON() : {}),
|
||||
...hit
|
||||
};
|
||||
});
|
||||
data.hits = populatedHits;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Push new document to Meili
|
||||
async addObjectToMeili() {
|
||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||
// NOTE: MeiliSearch does not allow | in primary key, so we replace it with - for Bing convoIds
|
||||
// object.conversationId = object.conversationId.replace(/\|/g, '-');
|
||||
if (object.conversationId && object.conversationId.includes('|')) {
|
||||
object.conversationId = object.conversationId.replace(/\|/g, '--');
|
||||
}
|
||||
|
||||
try {
|
||||
// console.log('Adding document to Meili', object);
|
||||
await index.addDocuments([object]);
|
||||
} catch (error) {
|
||||
// console.log('Error adding document to Meili');
|
||||
// console.error(error);
|
||||
}
|
||||
|
||||
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
|
||||
}
|
||||
|
||||
// Update an existing document in Meili
|
||||
async updateObjectToMeili() {
|
||||
const object = _.pick(this.toJSON(), attributesToIndex);
|
||||
await index.updateDocuments([object]);
|
||||
}
|
||||
|
||||
// Delete a document from Meili
|
||||
async deleteObjectFromMeili() {
|
||||
await index.deleteDocument(this._id);
|
||||
}
|
||||
|
||||
// * schema.post('save')
|
||||
postSaveHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili();
|
||||
} else {
|
||||
this.addObjectToMeili();
|
||||
}
|
||||
}
|
||||
|
||||
// * schema.post('update')
|
||||
postUpdateHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili();
|
||||
}
|
||||
}
|
||||
|
||||
// * schema.post('remove')
|
||||
postRemoveHook() {
|
||||
if (this._meiliIndex) {
|
||||
this.deleteObjectFromMeili();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MeiliMongooseModel;
|
||||
};
|
||||
|
||||
module.exports = function mongoMeili(schema, options) {
|
||||
// Vaidate Options for mongoMeili
|
||||
validateOptions(options);
|
||||
|
||||
// Add meiliIndex to schema
|
||||
schema.add({
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
select: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { host, apiKey, indexName, primaryKey } = options;
|
||||
|
||||
// Setup MeiliSearch Client
|
||||
const client = new MeiliSearch({ host, apiKey });
|
||||
|
||||
// Asynchronously create the index
|
||||
client.createIndex(indexName, { primaryKey });
|
||||
|
||||
// Setup the index to search for this schema
|
||||
const index = client.index(indexName);
|
||||
|
||||
const attributesToIndex = [
|
||||
..._.reduce(
|
||||
schema.obj,
|
||||
function (results, value, key) {
|
||||
return value.meiliIndex ? [...results, key] : results;
|
||||
// }, []), '_id'];
|
||||
},
|
||||
[]
|
||||
)
|
||||
];
|
||||
|
||||
schema.loadClass(createMeiliMongooseModel({ index, indexName, client, attributesToIndex }));
|
||||
|
||||
// Register hooks
|
||||
schema.post('save', function (doc) {
|
||||
doc.postSaveHook();
|
||||
});
|
||||
schema.post('update', function (doc) {
|
||||
doc.postUpdateHook();
|
||||
});
|
||||
schema.post('remove', function (doc) {
|
||||
doc.postRemoveHook();
|
||||
});
|
||||
schema.post('deleteMany', function () {
|
||||
// console.log('deleteMany hook', doc);
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||
console.log('Syncing convos...');
|
||||
mongoose.model('Conversation').syncWithMeili();
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||
console.log('Syncing messages...');
|
||||
mongoose.model('Message').syncWithMeili();
|
||||
}
|
||||
});
|
||||
schema.post('findOneAndUpdate', function (doc) {
|
||||
doc.postSaveHook();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../plugins/mongoMeili');
|
||||
const convoSchema = mongoose.Schema(
|
||||
{
|
||||
conversationId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'New Chat',
|
||||
meiliIndex: true
|
||||
},
|
||||
jailbreakConversationId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
conversationSignature: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
clientId: {
|
||||
type: String
|
||||
},
|
||||
invocationId: {
|
||||
type: String
|
||||
},
|
||||
toneStyle: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String
|
||||
},
|
||||
suggestions: [{ type: String }],
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
convoSchema.plugin(mongoMeili, {
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY,
|
||||
indexName: 'convos', // Will get created automatically if it doesn't exist already
|
||||
primaryKey: 'conversationId'
|
||||
});
|
||||
}
|
||||
|
||||
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||
|
||||
module.exports = Conversation;
|
||||
@@ -0,0 +1,71 @@
|
||||
const mongoose = require('mongoose');
|
||||
const mongoMeili = require('../plugins/mongoMeili');
|
||||
const messageSchema = mongoose.Schema(
|
||||
{
|
||||
messageId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true,
|
||||
index: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
conversationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
conversationSignature: {
|
||||
type: String
|
||||
// required: true
|
||||
},
|
||||
clientId: {
|
||||
type: String
|
||||
},
|
||||
invocationId: {
|
||||
type: String
|
||||
},
|
||||
parentMessageId: {
|
||||
type: String
|
||||
// required: true
|
||||
},
|
||||
sender: {
|
||||
type: String,
|
||||
required: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
meiliIndex: true
|
||||
},
|
||||
isCreatedByUser: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
select: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
messageSchema.plugin(mongoMeili, {
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY,
|
||||
indexName: 'messages',
|
||||
primaryKey: 'messageId'
|
||||
});
|
||||
}
|
||||
|
||||
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ignore": [
|
||||
"api/data/",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
Generated
+9931
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.2.0",
|
||||
"description": "",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
"start": "node server/index.js",
|
||||
"server-dev": "npx nodemon server/index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/danny-avila/chatgpt-clone.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/danny-avila/chatgpt-clone/issues"
|
||||
},
|
||||
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||
"dependencies": {
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.33.1",
|
||||
"axios": "^1.3.4",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"html": "^1.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"keyv-file": "^0.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.31.1",
|
||||
"mongoose": "^6.9.0",
|
||||
"openai": "^3.1.0",
|
||||
"sanitize": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20",
|
||||
"path": "^0.12.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//handle duplicates
|
||||
const handleDuplicateKeyError = (err, res) => {
|
||||
const field = Object.keys(err.keyValue);
|
||||
const code = 409;
|
||||
const error = `An document with that ${field} already exists.`;
|
||||
console.log('congrats you hit the duped keys error');
|
||||
res.status(code).send({ messages: error, fields: field });
|
||||
};
|
||||
|
||||
//handle validation errors
|
||||
const handleValidationError = (err, res) => {
|
||||
console.log('congrats you hit the validation middleware');
|
||||
let errors = Object.values(err.errors).map(el => el.message);
|
||||
let fields = Object.values(err.errors).map(el => el.path);
|
||||
let code = 400;
|
||||
if (errors.length > 1) {
|
||||
const formattedErrors = errors.join(' ');
|
||||
res.status(code).send({ messages: formattedErrors, fields: fields });
|
||||
} else {
|
||||
res.status(code).send({ messages: errors, fields: fields });
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (err, req, res, next) => {
|
||||
try {
|
||||
console.log('congrats you hit the error middleware');
|
||||
if (err.name === 'ValidationError') return (err = handleValidationError(err, res));
|
||||
if (err.code && err.code == 11000) return (err = handleDuplicateKeyError(err, res));
|
||||
} catch (err) {
|
||||
res.status(500).send('An unknown error occurred.');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const connectDb = require('../lib/db/connectDb');
|
||||
const migrateDb = require('../lib/db/migrateDb');
|
||||
const indexSync = require('../lib/db/indexSync');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const routes = require('./routes');
|
||||
const errorController = require('./controllers/errorController');
|
||||
|
||||
const port = process.env.PORT || 3080;
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
|
||||
(async () => {
|
||||
await connectDb();
|
||||
console.log('Connected to MongoDB');
|
||||
await migrateDb();
|
||||
await indexSync();
|
||||
|
||||
const app = express();
|
||||
app.use(errorController);
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(projectPath, 'public')));
|
||||
app.set('trust proxy', 1); // trust first proxy
|
||||
app.use(
|
||||
session({
|
||||
secret: 'chatgpt-clone-random-secrect',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 7 days
|
||||
})
|
||||
);
|
||||
|
||||
/* chore: potential redirect error here, can only comment out this block;
|
||||
comment back in if using auth routes i guess */
|
||||
// app.get('/', routes.authenticatedOrRedirect, function (req, res) {
|
||||
// console.log(path.join(projectPath, 'public', 'index.html'));
|
||||
// res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||
// });
|
||||
|
||||
app.get('/api/me', function (req, res) {
|
||||
if (userSystemEnabled) {
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user) res.send(JSON.stringify({ username: user?.username, display: user?.display }));
|
||||
else res.send(JSON.stringify(null));
|
||||
} else {
|
||||
res.send(JSON.stringify({ username: 'anonymous_user', display: 'Anonymous User' }));
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/api/search', routes.authenticatedOr401, routes.search);
|
||||
app.use('/api/ask', routes.authenticatedOr401, routes.ask);
|
||||
app.use('/api/messages', routes.authenticatedOr401, routes.messages);
|
||||
app.use('/api/convos', routes.authenticatedOr401, routes.convos);
|
||||
app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts);
|
||||
app.use('/api/prompts', routes.authenticatedOr401, routes.prompts);
|
||||
app.use('/auth', routes.auth);
|
||||
|
||||
app.get('/api/models', function (req, res) {
|
||||
const hasOpenAI = !!process.env.OPENAI_KEY;
|
||||
const hasChatGpt = !!process.env.CHATGPT_TOKEN;
|
||||
const hasBing = !!process.env.BING_TOKEN;
|
||||
|
||||
res.send(JSON.stringify({ hasOpenAI, hasChatGpt, hasBing }));
|
||||
});
|
||||
|
||||
app.get('/*', routes.authenticatedOrRedirect, function (req, res) {
|
||||
res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
if (host == '0.0.0.0')
|
||||
console.log(
|
||||
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
|
||||
);
|
||||
else console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||
});
|
||||
})();
|
||||
|
||||
let messageCount = 0;
|
||||
process.on('uncaughtException', err => {
|
||||
if (!err.message.includes('fetch failed')) {
|
||||
console.error('There was an uncaught error:', err.message);
|
||||
}
|
||||
|
||||
if (err.message.includes('fetch failed')) {
|
||||
if (messageCount === 0) {
|
||||
console.error('Meilisearch error, search will be disabled');
|
||||
messageCount++;
|
||||
}
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const askBing = require('./askBing');
|
||||
const askSydney = require('./askSydney');
|
||||
const { titleConvo, askClient, browserClient, customClient } = require('../../app/');
|
||||
const { saveMessage, getConvoTitle, saveConvo, updateConvo } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.use('/bing', askBing);
|
||||
router.use('/sydney', askSydney);
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId = null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const userMessageId = crypto.randomUUID();
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...userMessage,
|
||||
...convo
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { ...userMessage, model, ...convo });
|
||||
}
|
||||
|
||||
return await ask({ userMessage, model, convo, preSendRequest: true, overrideParentMessageId, req, res });
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
userMessage,
|
||||
overrideParentMessageId = null,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
const {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
const client = model === 'chatgpt' ? askClient : model === 'chatgptCustom' ? customClient : browserClient;
|
||||
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => abortController.abort());
|
||||
let gptResponse = await client({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, { res, text }),
|
||||
convo: { parentMessageId: userParentMessageId, conversationId, ...convo },
|
||||
...convo,
|
||||
abortController
|
||||
});
|
||||
|
||||
gptResponse.text = gptResponse.response;
|
||||
console.log('CLIENT RESPONSE', gptResponse);
|
||||
|
||||
if (!gptResponse.parentMessageId) {
|
||||
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
|
||||
delete gptResponse.response;
|
||||
}
|
||||
|
||||
gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model;
|
||||
gptResponse.model = model;
|
||||
gptResponse.text = await handleText(gptResponse);
|
||||
if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.chatGptLabel = convo.chatGptLabel;
|
||||
}
|
||||
|
||||
if (convo.promptPrefix?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.promptPrefix = convo.promptPrefix;
|
||||
}
|
||||
|
||||
gptResponse.parentMessageId = overrideParentMessageId || userMessageId;
|
||||
|
||||
if (model === 'chatgptBrowser' && userParentMessageId.startsWith('000')) {
|
||||
await saveMessage({ ...userMessage, conversationId: gptResponse.conversationId });
|
||||
}
|
||||
|
||||
await saveMessage(gptResponse);
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
...gptResponse,
|
||||
oldConvoId: model === 'chatgptBrowser' && conversationId
|
||||
});
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: gptResponse
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response: gptResponse });
|
||||
await updateConvo(req?.session?.user?.username, {
|
||||
conversationId: model === 'chatgptBrowser' ? gptResponse.conversationId : conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,190 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { titleConvo, askBing } = require('../../app/');
|
||||
const { saveBingMessage, getConvoTitle, saveConvo } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId=null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) {
|
||||
return handleError(res, { text: 'Prompt empty or too short' });
|
||||
}
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
|
||||
const userMessageId = convo.messageId;
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
let userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...convo,
|
||||
...userMessage
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveBingMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { model, ...convo, ...userMessage });
|
||||
}
|
||||
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
overrideParentMessageId = null,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => {
|
||||
console.log('The client has disconnected.');
|
||||
// 执行其他操作
|
||||
abortController.abort();
|
||||
})
|
||||
|
||||
let response = await askBing({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
convo: {
|
||||
...convo,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId
|
||||
},
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('BING RESPONSE', response);
|
||||
// console.dir(response, { depth: null });
|
||||
|
||||
userMessage.conversationSignature =
|
||||
convo.conversationSignature || response.conversationSignature;
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
userMessage.invocationId = response.invocationId;
|
||||
userMessage.messageId = response.details.requestId || userMessageId;
|
||||
if (!overrideParentMessageId)
|
||||
await saveBingMessage({ oldMessageId: userMessageId, ...userMessage });
|
||||
|
||||
// Bing API will not use our conversationId at the first time,
|
||||
// so change the placeholder conversationId to the real one.
|
||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
||||
// but in this situation, don't change the conversationId, but create new convo.
|
||||
if (conversationId != userMessage.conversationId && isNewConversation)
|
||||
await saveConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId: conversationId,
|
||||
newConversationId: userMessage.conversationId
|
||||
}
|
||||
);
|
||||
conversationId = userMessage.conversationId;
|
||||
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
// delete response.response;
|
||||
// response.id = response.details.messageId;
|
||||
response.suggestions =
|
||||
response.details.suggestedResponses &&
|
||||
response.details.suggestedResponses.map((s) => s.text);
|
||||
response.sender = model;
|
||||
// response.final = true;
|
||||
|
||||
response.messageId = response.details.messageId;
|
||||
// override the parentMessageId, for the regeneration.
|
||||
response.parentMessageId =
|
||||
overrideParentMessageId || response.details.requestId || userMessageId;
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
await saveBingMessage(response);
|
||||
await saveConvo(req?.session?.user?.username, { model, chatGptLabel: null, promptPrefix: null, ...convo, ...response });
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await saveConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
...convo,
|
||||
...response,
|
||||
conversationId,
|
||||
title
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// await deleteMessages({ messageId: userMessageId });
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveBingMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,198 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const router = express.Router();
|
||||
const { titleConvo, askSydney } = require('../../app/');
|
||||
const { saveBingMessage, saveConvo, updateConvo, getConvoTitle } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
model,
|
||||
text,
|
||||
overrideParentMessageId=null,
|
||||
parentMessageId,
|
||||
conversationId: oldConversationId,
|
||||
...convo
|
||||
} = req.body;
|
||||
if (text.length === 0) {
|
||||
return handleError(res, { text: 'Prompt empty or too short' });
|
||||
}
|
||||
|
||||
const conversationId = oldConversationId || crypto.randomUUID();
|
||||
const isNewConversation = !oldConversationId;
|
||||
|
||||
const userMessageId = convo.messageId;
|
||||
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
let userMessage = {
|
||||
messageId: userMessageId,
|
||||
sender: 'User',
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
console.log('ask log', {
|
||||
model,
|
||||
...convo,
|
||||
...userMessage
|
||||
});
|
||||
|
||||
if (!overrideParentMessageId) {
|
||||
await saveBingMessage(userMessage);
|
||||
await saveConvo(req?.session?.user?.username, { model, ...convo, ...userMessage });
|
||||
}
|
||||
|
||||
return await ask({
|
||||
isNewConversation,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest: true,
|
||||
overrideParentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
isNewConversation,
|
||||
overrideParentMessageId = null,
|
||||
userMessage,
|
||||
model,
|
||||
convo,
|
||||
preSendRequest = true,
|
||||
req,
|
||||
res
|
||||
}) => {
|
||||
let {
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
messageId: userMessageId
|
||||
} = userMessage;
|
||||
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
|
||||
|
||||
try {
|
||||
const progressCallback = createOnProgress();
|
||||
|
||||
const abortController = new AbortController();
|
||||
res.on('close', () => {
|
||||
console.log('The client has disconnected.');
|
||||
// 执行其他操作
|
||||
abortController.abort();
|
||||
})
|
||||
|
||||
let response = await askSydney({
|
||||
text,
|
||||
onProgress: progressCallback.call(null, model, {
|
||||
res,
|
||||
text,
|
||||
parentMessageId: overrideParentMessageId || userMessageId
|
||||
}),
|
||||
convo: {
|
||||
...convo,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId
|
||||
},
|
||||
abortController
|
||||
});
|
||||
|
||||
console.log('SYDNEY RESPONSE', response);
|
||||
// console.dir(response, { depth: null });
|
||||
|
||||
userMessage.conversationSignature =
|
||||
convo.conversationSignature || response.conversationSignature;
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
userMessage.invocationId = response.invocationId;
|
||||
// Unlike gpt and bing, Sydney will never accept our given userMessage.messageId, it will generate its own one.
|
||||
userMessage.messageId = response.parentMessageId || userMessageId;
|
||||
|
||||
// Save sydney response
|
||||
// response.id = response.messageId;
|
||||
response.invocationId = convo.invocationId ? convo.invocationId + 1 : 1;
|
||||
response.conversationId = conversationId ? conversationId : crypto.randomUUID();
|
||||
response.conversationSignature = convo.conversationSignature
|
||||
? convo.conversationSignature
|
||||
: crypto.randomUUID();
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
// delete response.response;
|
||||
response.suggestions =
|
||||
response.details.suggestedResponses &&
|
||||
response.details.suggestedResponses.map((s) => s.text);
|
||||
response.sender = model;
|
||||
// response.final = true;
|
||||
|
||||
// override the parentMessageId, for the regeneration.
|
||||
response.parentMessageId =
|
||||
overrideParentMessageId || response.parentMessageId || userMessageId;
|
||||
|
||||
// Save user message
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
if (!overrideParentMessageId)
|
||||
await saveBingMessage({ oldMessageId: userMessageId, ...userMessage });
|
||||
|
||||
// Bing API will not use our conversationId at the first time,
|
||||
// so change the placeholder conversationId to the real one.
|
||||
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
|
||||
// but in this situation, don't change the conversationId, but create new convo.
|
||||
if (conversationId != userMessage.conversationId && isNewConversation)
|
||||
await updateConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId: conversationId,
|
||||
newConversationId: userMessage.conversationId
|
||||
}
|
||||
);
|
||||
conversationId = userMessage.conversationId;
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
// Save sydney response & convo, then send
|
||||
await saveBingMessage(response);
|
||||
await updateConvo(req?.session?.user?.username, { model, chatGptLabel: null, promptPrefix: null, ...convo, ...response });
|
||||
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req?.session?.user?.username, conversationId),
|
||||
final: true,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await updateConvo(
|
||||
req?.session?.user?.username,
|
||||
{
|
||||
conversationId,
|
||||
title
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// await deleteMessages({ messageId: userMessageId });
|
||||
const errorMessage = {
|
||||
messageId: crypto.randomUUID(),
|
||||
sender: model,
|
||||
conversationId,
|
||||
parentMessageId: overrideParentMessageId || userMessageId,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveBingMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,57 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const authYourLogin = require('./authYourLogin');
|
||||
const userSystemEnabled = !!process.env.ENABLE_USER_SYSTEM || false;
|
||||
|
||||
router.get('/login', function (req, res) {
|
||||
if (userSystemEnabled) {
|
||||
res.redirect('/auth/your_login_page');
|
||||
} else {
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/logout', function (req, res) {
|
||||
// clear the session
|
||||
req.session.user = null;
|
||||
|
||||
req.session.save(function () {
|
||||
if (userSystemEnabled) {
|
||||
res.redirect('/auth/your_login_page/logout');
|
||||
} else {
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const authenticatedOr401 = (req, res, next) => {
|
||||
if (userSystemEnabled) {
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).end();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const authenticatedOrRedirect = (req, res, next) => {
|
||||
if (userSystemEnabled) {
|
||||
const user = req?.session?.user;
|
||||
|
||||
if (user) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/auth/login').end();
|
||||
}
|
||||
} else next();
|
||||
};
|
||||
|
||||
if (userSystemEnabled) {
|
||||
router.use('/your_login_page', authYourLogin);
|
||||
}
|
||||
|
||||
module.exports = { router, authenticatedOr401, authenticatedOrRedirect };
|
||||
@@ -0,0 +1,44 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// WARNING!
|
||||
// THIS IS NOT A READY TO USE USER SYSTEM
|
||||
// PLEASE IMPLEMENT YOUR OWN USER SYSTEM
|
||||
|
||||
const userSystemEnabled = process.env.ENABLE_USER_SYSTEM || false;
|
||||
|
||||
// Logout
|
||||
router.get('/logout', (req, res) => {
|
||||
// Do anything you want
|
||||
console.warn('logout not implemented!');
|
||||
|
||||
// finish
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
// Login
|
||||
router.get('/', async (req, res) => {
|
||||
// Do anything you want
|
||||
console.warn('login not implemented! Automatic passed as sample user');
|
||||
|
||||
// save the user info into session
|
||||
// username will be used in db
|
||||
// display will be used in UI
|
||||
if (userSystemEnabled) {
|
||||
req.session.user = {
|
||||
username: null, // was 'sample_user', but would break previous relationship with previous conversations before v0.1.0
|
||||
display: 'Sample User'
|
||||
};
|
||||
}
|
||||
|
||||
req.session.save(function (error) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
res.send(`<h1>Login Failed. An error occurred. Please see the server logs for details.</h1>`);
|
||||
} else {
|
||||
res.redirect('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,55 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { titleConvo } = require('../../app/');
|
||||
const { getConvo, saveConvo, getConvoTitle } = require('../../models');
|
||||
const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation');
|
||||
const { getMessages } = require('../../models/Message');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const pageNumber = req.query.pageNumber || 1;
|
||||
res.status(200).send(await getConvosByPage(req?.session?.user?.username, pageNumber));
|
||||
});
|
||||
|
||||
router.get('/:conversationId', async (req, res) => {
|
||||
const { conversationId } = req.params;
|
||||
const convo = await getConvo(req?.session?.user?.username, conversationId);
|
||||
|
||||
if (convo) res.status(200).send(convo.toObject());
|
||||
else res.status(404).end();
|
||||
});
|
||||
|
||||
router.post('/clear', async (req, res) => {
|
||||
let filter = {};
|
||||
const { conversationId, source } = req.body.arg;
|
||||
if (conversationId) {
|
||||
filter = { conversationId };
|
||||
}
|
||||
|
||||
console.log('source:', source);
|
||||
|
||||
if (source === 'button' && !conversationId) {
|
||||
return res.status(200).send('No conversationId provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const dbResponse = await deleteConvos(req?.session?.user?.username, filter);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/update', async (req, res) => {
|
||||
const update = req.body.arg;
|
||||
|
||||
try {
|
||||
const dbResponse = await updateConvo(req?.session?.user?.username, update);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,67 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
updateByLabel,
|
||||
deleteCustomGpts
|
||||
} = require('../../models');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => {
|
||||
model = model.toObject();
|
||||
model._id = model._id.toString();
|
||||
return model;
|
||||
});
|
||||
res.status(200).send(models);
|
||||
});
|
||||
|
||||
router.post('/delete', async (req, res) => {
|
||||
const { arg } = req.body;
|
||||
|
||||
try {
|
||||
await deleteCustomGpts(req?.session?.user?.username, arg);
|
||||
const models = (await getCustomGpts(req?.session?.user?.username)).map((model) => {
|
||||
model = model.toObject();
|
||||
model._id = model._id.toString();
|
||||
return model;
|
||||
});
|
||||
res.status(201).send(models);
|
||||
// res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
// router.post('/create', async (req, res) => {
|
||||
// const payload = req.body.arg;
|
||||
|
||||
// try {
|
||||
// const dbResponse = await createCustomGpt(payload);
|
||||
// res.status(201).send(dbResponse);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// res.status(500).send(error);
|
||||
// }
|
||||
// });
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const update = req.body.arg;
|
||||
|
||||
let setter = updateCustomGpt;
|
||||
|
||||
if (update.prevLabel) {
|
||||
setter = updateByLabel;
|
||||
}
|
||||
|
||||
try {
|
||||
const dbResponse = await setter(req?.session?.user?.username, update);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,96 @@
|
||||
const _ = require('lodash');
|
||||
const citationRegex = /\[\^\d+?\^]/g;
|
||||
const backtick = /(?<!`)[`](?!`)/g;
|
||||
// const singleBacktick = /(?<!`)[`](?!`)/;
|
||||
const cursorDefault = '<span className="result-streaming">█</span>';
|
||||
const { getCitations, citeText } = require('../../app/');
|
||||
|
||||
const handleError = (res, message) => {
|
||||
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
res.end();
|
||||
};
|
||||
|
||||
const sendMessage = (res, message) => {
|
||||
if (message.length === 0) {
|
||||
return;
|
||||
}
|
||||
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
};
|
||||
|
||||
const createOnProgress = () => {
|
||||
let i = 0;
|
||||
let code = '';
|
||||
let tokens = '';
|
||||
let precode = '';
|
||||
let blockCount = 0;
|
||||
let codeBlock = false;
|
||||
let cursor = cursorDefault;
|
||||
|
||||
const progressCallback = async (partial, { res, text, bing = false, ...rest }) => {
|
||||
let chunk = partial === text ? '' : partial;
|
||||
tokens += chunk;
|
||||
precode += chunk;
|
||||
tokens = tokens.replaceAll('[DONE]', '');
|
||||
|
||||
if (codeBlock) {
|
||||
code += chunk;
|
||||
}
|
||||
|
||||
if (precode.includes('```') && codeBlock) {
|
||||
codeBlock = false;
|
||||
cursor = cursorDefault;
|
||||
precode = precode.replace(/```/g, '');
|
||||
code = '';
|
||||
}
|
||||
|
||||
if (precode.includes('```') && code === '') {
|
||||
precode = precode.replace(/```/g, '');
|
||||
codeBlock = true;
|
||||
blockCount++;
|
||||
cursor = blockCount > 1 ? '█\n\n```' : '█\n\n';
|
||||
}
|
||||
|
||||
const backticks = precode.match(backtick);
|
||||
if (backticks && !codeBlock && cursor === cursorDefault) {
|
||||
precode = precode.replace(backtick, '');
|
||||
cursor = '█';
|
||||
}
|
||||
|
||||
if (tokens.match(/^\n/)) {
|
||||
tokens = tokens.replace(/^\n/, '');
|
||||
}
|
||||
|
||||
if (bing) {
|
||||
tokens = citeText(tokens, true);
|
||||
}
|
||||
|
||||
sendMessage(res, { text: tokens + cursor, message: true, initial: i === 0, ...rest });
|
||||
i++;
|
||||
};
|
||||
|
||||
const onProgress = (model, opts) => {
|
||||
const bingModels = new Set(['bingai', 'sydney']);
|
||||
return _.partialRight(progressCallback, { ...opts, bing: bingModels.has(model) });
|
||||
};
|
||||
|
||||
return onProgress;
|
||||
};
|
||||
|
||||
const handleText = async (response, bing = false) => {
|
||||
let { text } = response;
|
||||
// text = await detectCode(text);
|
||||
response.text = text;
|
||||
|
||||
if (bing) {
|
||||
// const hasCitations = response.response.match(citationRegex)?.length > 0;
|
||||
const links = getCitations(response);
|
||||
if (response.text.match(citationRegex)?.length > 0) {
|
||||
text = citeText(response);
|
||||
}
|
||||
text += links?.length > 0 ? `\n<small>${links}</small>` : '';
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
module.exports = { handleError, sendMessage, createOnProgress, handleText };
|
||||
@@ -0,0 +1,9 @@
|
||||
const ask = require('./ask');
|
||||
const messages = require('./messages');
|
||||
const convos = require('./convos');
|
||||
const customGpts = require('./customGpts');
|
||||
const prompts = require('./prompts');
|
||||
const search = require('./search');
|
||||
const { router: auth, authenticatedOr401, authenticatedOrRedirect } = require('./auth');
|
||||
|
||||
module.exports = { search, ask, messages, convos, customGpts, prompts, auth, authenticatedOr401, authenticatedOrRedirect };
|
||||
@@ -0,0 +1,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getMessages } = require('../../models/Message');
|
||||
|
||||
router.get('/:conversationId', async (req, res) => {
|
||||
const { conversationId } = req.params;
|
||||
res.status(200).send(await getMessages({ conversationId }));
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,14 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { savePrompt, getPrompts, deletePrompts } = require('../../models/Prompt');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
let filter = {};
|
||||
// const { search } = req.body.arg;
|
||||
// if (!!search) {
|
||||
// filter = { conversationId };
|
||||
// }
|
||||
res.status(200).send(await getPrompts(filter));
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,123 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { Message } = require('../../models/Message');
|
||||
const { Conversation, getConvosQueried } = require('../../models/Conversation');
|
||||
const { reduceHits } = require('../../lib/utils/reduceHits');
|
||||
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||
const cache = new Map();
|
||||
|
||||
router.get('/sync', async function (req, res) {
|
||||
await Message.syncWithMeili();
|
||||
await Conversation.syncWithMeili();
|
||||
res.send('synced');
|
||||
});
|
||||
|
||||
router.get('/', async function (req, res) {
|
||||
try {
|
||||
let user = req?.session?.user?.username;
|
||||
user = user ?? null;
|
||||
const { q } = req.query;
|
||||
const pageNumber = req.query.pageNumber || 1;
|
||||
const key = `${user || ''}${q}`;
|
||||
|
||||
if (cache.has(key)) {
|
||||
console.log('cache hit', key);
|
||||
const cached = cache.get(key);
|
||||
const { pages, pageSize, messages } = cached;
|
||||
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||
return;
|
||||
} else {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// const message = await Message.meiliSearch(q);
|
||||
const messages = (
|
||||
await Message.meiliSearch(
|
||||
q,
|
||||
{
|
||||
attributesToHighlight: ['text'],
|
||||
highlightPreTag: '**',
|
||||
highlightPostTag: '**'
|
||||
},
|
||||
true
|
||||
)
|
||||
).hits.map(message => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return {
|
||||
...rest,
|
||||
searchResult: true,
|
||||
text: _formatted.text
|
||||
};
|
||||
});
|
||||
const titles = (await Conversation.meiliSearch(q)).hits;
|
||||
const sortedHits = reduceHits(messages, titles);
|
||||
// debugging:
|
||||
// console.log('user:', user, 'message hits:', messages.length, 'convo hits:', titles.length);
|
||||
// console.log('sorted hits:', sortedHits.length);
|
||||
const result = await getConvosQueried(user, sortedHits, pageNumber);
|
||||
|
||||
const activeMessages = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
let message = messages[i];
|
||||
if (message.conversationId.includes('--')) {
|
||||
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
|
||||
}
|
||||
if (result.convoMap[message.conversationId] && !message.error) {
|
||||
const convo = result.convoMap[message.conversationId];
|
||||
const { title, chatGptLabel, model } = convo;
|
||||
message = { ...message, ...{ title, chatGptLabel, model } };
|
||||
activeMessages.push(message);
|
||||
}
|
||||
}
|
||||
result.messages = activeMessages;
|
||||
if (result.cache) {
|
||||
result.cache.messages = activeMessages;
|
||||
cache.set(key, result.cache);
|
||||
delete result.cache;
|
||||
}
|
||||
delete result.convoMap;
|
||||
// for debugging
|
||||
// console.log(result, messages.length);
|
||||
res.status(200).send(result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).send({ message: 'Error searching' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/clear', async function (req, res) {
|
||||
await Message.resetIndex();
|
||||
res.send('cleared');
|
||||
});
|
||||
|
||||
router.get('/test', async function (req, res) {
|
||||
const { q } = req.query;
|
||||
const messages = (await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)).hits.map(
|
||||
message => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return { ...rest, searchResult: true, text: _formatted.text };
|
||||
}
|
||||
);
|
||||
res.send(messages);
|
||||
});
|
||||
|
||||
router.get('/enable', async function (req, res) {
|
||||
let result = false;
|
||||
try {
|
||||
const client = new MeiliSearch({
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY
|
||||
});
|
||||
|
||||
const { status } = await client.health();
|
||||
// console.log(`Meilisearch: ${status}`);
|
||||
result = status === 'available' && !!process.env.SEARCH;
|
||||
return res.send(result);
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
return res.send(false);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,21 +0,0 @@
|
||||
meta {
|
||||
name: Get Messages
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.chat_stream.get_messages?channel_id=general
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
channel_id: general
|
||||
~channel_id: admin-private
|
||||
~channel_id: does-not-exist
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
meta {
|
||||
name: Get Channel Members
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.chat.get_channel_members?channel_id=general
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
channel_id: general
|
||||
~channel_id: admin-private
|
||||
~channel_id: does-not-exist
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
meta {
|
||||
name: Get Preview Link
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.preview_links.get_preview_link?urls=["https://ravenapp.info"]
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
urls: ["https://ravenapp.info"]
|
||||
~urls: ["http://192.168.0.0"]
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: Get List
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.raven_users.get_list
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
meta {
|
||||
name: React
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.reactions.react?message_id=7a46635aff&reaction=🐶
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
message_id: 7a46635aff
|
||||
reaction: 🐶
|
||||
~message_id: 4ba126d7ba
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
|
||||
docs {
|
||||
API to toggle a reaction to a Message
|
||||
|
||||
The API first checks if the user has permission for the message. If yes, then it checks if the user has already reacted to the message. If not, it creates a new reaction, else it deletes the existing reaction.
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
meta {
|
||||
name: Get Active Users
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.user_availability.get_active_users
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
meta {
|
||||
name: Refresh User Active State
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/raven.api.user_availability.refresh_user_active_state
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
~deactivate: true
|
||||
}
|
||||
|
||||
headers {
|
||||
Authorization: token {{api_key}}:{{api_secret}}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Raven",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
vars {
|
||||
url: http://localhost
|
||||
port: 8000
|
||||
api_key: a0fa913c5571c73
|
||||
}
|
||||
vars:secret [
|
||||
api_secret
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
meta {
|
||||
name: login
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}:{{port}}/api/method/login?usr=Administrator&pwd=admin
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
query {
|
||||
usr: Administrator
|
||||
pwd: admin
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
ios/
|
||||
android/
|
||||
.expo/
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>API_KEY</key>
|
||||
<string>AIzaSyBeMvrmiY5bXIjAUlR03sQKH5NGNkS7d6Y</string>
|
||||
<key>GCM_SENDER_ID</key>
|
||||
<string>257310726313</string>
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>raven.thecommit.company</string>
|
||||
<key>PROJECT_ID</key>
|
||||
<string>raven-c2659</string>
|
||||
<key>STORAGE_BUCKET</key>
|
||||
<string>raven-c2659.firebasestorage.app</string>
|
||||
<key>IS_ADS_ENABLED</key>
|
||||
<false></false>
|
||||
<key>IS_ANALYTICS_ENABLED</key>
|
||||
<false></false>
|
||||
<key>IS_APPINVITE_ENABLED</key>
|
||||
<true></true>
|
||||
<key>IS_GCM_ENABLED</key>
|
||||
<true></true>
|
||||
<key>IS_SIGNIN_ENABLED</key>
|
||||
<true></true>
|
||||
<key>GOOGLE_APP_ID</key>
|
||||
<string>1:257310726313:ios:a192c58ca3307514a631b8</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Raven",
|
||||
"slug": "Raven",
|
||||
"scheme": "raven.thecommit.company",
|
||||
"version": "1.1.2",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"appleTeamId": "W6346TTZQX",
|
||||
"bundleIdentifier": "raven.thecommit.company",
|
||||
"config": {
|
||||
"usesNonExemptEncryption": false
|
||||
},
|
||||
"googleServicesFile": "./GoogleService-Info.plist",
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": [
|
||||
"remote-notification",
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"entitlements": {
|
||||
"aps-environment": "production"
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"softwareKeyboardLayoutMode": "pan",
|
||||
"package": "raven.thecommit.company",
|
||||
"googleServicesFile": "./google-services.json"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router"
|
||||
],
|
||||
[
|
||||
"expo-secure-store"
|
||||
],
|
||||
[
|
||||
"expo-font",
|
||||
{
|
||||
"fonts": [
|
||||
"./assets/fonts/CalSans-SemiBold.otf"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@react-native-firebase/app",
|
||||
"@react-native-firebase/messaging",
|
||||
"@react-native-firebase/crashlytics",
|
||||
"@react-native-firebase/perf",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"ios": {
|
||||
"useFrameworks": "static"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-video"
|
||||
],
|
||||
"extra": {
|
||||
"router": {
|
||||
"origin": false
|
||||
},
|
||||
"eas": {
|
||||
"projectId": "3a173ddd-f22a-4f81-8279-40dcee6c29d5"
|
||||
}
|
||||
},
|
||||
"owner": "the-commit-company"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { router, Stack } from 'expo-router';
|
||||
import { View } from 'react-native';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
|
||||
const { getItem } = useAsyncStorage(`default-site`)
|
||||
|
||||
const handleGoHome = () => {
|
||||
getItem().then(site => {
|
||||
if (site) {
|
||||
router.replace(`/${site}`)
|
||||
} else {
|
||||
router.replace('/landing')
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<View className='flex-1 bg-background justify-center gap-3 items-center'>
|
||||
<Text className='text-3xl text-foreground'>This screen doesn't exist.</Text>
|
||||
<View className='h-2' />
|
||||
<Button onPress={handleGoHome}>
|
||||
<Text>Go Home</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Stack, Tabs } from 'expo-router';
|
||||
import { SvgProps } from 'react-native-svg';
|
||||
import HomeIcon from '@assets/icons/HomeIcon.svg';
|
||||
import HomeOutlineIcon from '@assets/icons/HomeOutlineIcon.svg';
|
||||
import ProfileIcon from '@assets/icons/ProfileIcon.svg';
|
||||
import ProfileOutlineIcon from '@assets/icons/ProfileOutlineIcon.svg';
|
||||
import ChatIcon from '@assets/icons/ChatIcon.svg';
|
||||
import ChatOutlineIcon from '@assets/icons/ChatOutlineIcon.svg';
|
||||
import ThreadsIcon from '@assets/icons/ThreadsIcon.svg';
|
||||
import ThreadsOutlineIcon from '@assets/icons/ThreadsOutlineIcon.svg';
|
||||
import { useColorScheme } from '@hooks/useColorScheme'
|
||||
import { Platform } from 'react-native';
|
||||
import useUnreadThreadsCount from '@hooks/useUnreadThreadsCount';
|
||||
import useUnreadMessageCount from '@hooks/useUnreadMessageCount';
|
||||
|
||||
const tabBarBadgeStyle = {
|
||||
maxWidth: 8,
|
||||
maxHeight: 8,
|
||||
marginRight: 4,
|
||||
marginTop: 2,
|
||||
fontSize: 8,
|
||||
lineHeight: 9,
|
||||
alignSelf: undefined,
|
||||
}
|
||||
|
||||
export default function TabLayout() {
|
||||
|
||||
const { colors, colorScheme } = useColorScheme()
|
||||
const dark = colorScheme == "dark"
|
||||
|
||||
// Common styles
|
||||
const tabBarStyle = {
|
||||
backgroundColor: dark ? 'rgba(18, 18, 18, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: dark ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.05)',
|
||||
paddingTop: 4,
|
||||
marginBottom: Platform.OS === 'ios' ? 0 : 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: Platform.OS === 'ios' ? { width: 0, height: -2 } : undefined,
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 5,
|
||||
elevation: Platform.OS === 'ios' ? 5 : 0
|
||||
}
|
||||
|
||||
const headerStyle = {
|
||||
backgroundColor: dark ? 'rgba(18, 18, 18, 0)' : 'rgba(249, 249, 249, 1)',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: dark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0)',
|
||||
}
|
||||
|
||||
const { data: unreadThreads } = useUnreadThreadsCount()
|
||||
|
||||
const { unread_count } = useUnreadMessageCount()
|
||||
|
||||
const { hasUnreadMessages, hasUnreadDMs } = useMemo(() => {
|
||||
return {
|
||||
hasUnreadMessages: unread_count?.message.some(item => item.unread_count > 0),
|
||||
hasUnreadDMs: unread_count?.message.some(item => item.unread_count > 0 && item.is_direct_message === 1),
|
||||
}
|
||||
}, [unread_count])
|
||||
|
||||
const hasUnreadThreads = useMemo(() => {
|
||||
return unreadThreads?.message.some(item => item.unread_count > 0)
|
||||
}, [unreadThreads])
|
||||
|
||||
const tabBarIconStyle = (focused: boolean) => ({
|
||||
opacity: focused ? 1 : dark ? 0.8 : 0.7,
|
||||
})
|
||||
|
||||
const getTabBarIcon =
|
||||
(FilledIcon: React.FC<SvgProps>, OutlineIcon: React.FC<SvgProps>) =>
|
||||
({ color, focused }: { color: string; focused: boolean }) =>
|
||||
focused ? (
|
||||
<FilledIcon
|
||||
fill={color}
|
||||
style={tabBarIconStyle(focused)}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
) : (
|
||||
<OutlineIcon
|
||||
fill={color}
|
||||
style={tabBarIconStyle(focused)}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ headerShown: false, title: 'Home' }} />
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarStyle,
|
||||
tabBarActiveTintColor: dark ? '#FFFFFF' : colors.primary,
|
||||
tabBarInactiveTintColor: dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="home"
|
||||
options={{
|
||||
title: 'Home',
|
||||
headerShown: false,
|
||||
headerStyle,
|
||||
tabBarBadge: hasUnreadMessages ? '' : undefined,
|
||||
tabBarBadgeStyle,
|
||||
tabBarIcon: getTabBarIcon(HomeIcon, HomeOutlineIcon),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="direct-messages"
|
||||
options={{
|
||||
title: 'DMs',
|
||||
headerShown: false,
|
||||
headerStyle,
|
||||
tabBarBadge: hasUnreadDMs ? '' : undefined,
|
||||
tabBarBadgeStyle,
|
||||
tabBarIcon: getTabBarIcon(ChatIcon, ChatOutlineIcon),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="threads"
|
||||
options={{
|
||||
title: 'Threads',
|
||||
headerShown: false,
|
||||
headerStyle,
|
||||
tabBarBadge: hasUnreadThreads ? '' : undefined,
|
||||
tabBarBadgeStyle,
|
||||
tabBarIcon: getTabBarIcon(ThreadsIcon, ThreadsOutlineIcon),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: 'Profile',
|
||||
headerShown: false,
|
||||
headerStyle,
|
||||
tabBarIcon: getTabBarIcon(ProfileIcon, ProfileOutlineIcon),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
const DirectMessagesLayout = () => {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerStyle: { backgroundColor: colors.background } }}>
|
||||
<Stack.Screen name='index'
|
||||
options={{
|
||||
title: 'Direct Messages',
|
||||
headerLargeTitle: false
|
||||
}} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default DirectMessagesLayout
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ScrollView } from 'react-native';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import AllDMsList from '@components/features/channels/DMList/AllDMsList';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
export default function DirectMessages() {
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 5 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
>
|
||||
<AllDMsList />
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
const HomeLayout = () => {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<Stack initialRouteName='index'>
|
||||
<Stack.Screen name='index'
|
||||
options={{
|
||||
title: 'Home',
|
||||
headerShown: false,
|
||||
headerShadowVisible: false,
|
||||
headerStyle: { backgroundColor: colors.primary }
|
||||
}} />
|
||||
<Stack.Screen name='quick-search'
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerStyle: { backgroundColor: colors.background }
|
||||
}} />
|
||||
<Stack.Screen name='create-channel'
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerStyle: { backgroundColor: colors.background }
|
||||
}} />
|
||||
<Stack.Screen name='browse-channels'
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerStyle: { backgroundColor: colors.background }
|
||||
}} />
|
||||
<Stack.Screen name='create-dm'
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerStyle: { backgroundColor: colors.background }
|
||||
}} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeLayout
|
||||
@@ -1,136 +0,0 @@
|
||||
import { Link, router, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import CrossIcon from '@assets/icons/CrossIcon.svg';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Platform, Pressable, View } from 'react-native';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import SearchInput from '@components/common/SearchInput/SearchInput';
|
||||
import { ChannelIcon } from '@components/features/channels/ChannelList/ChannelIcon';
|
||||
import useGetChannels from '@raven/lib/hooks/useGetChannels';
|
||||
import { useState } from 'react';
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu';
|
||||
import HashIcon from '@assets/icons/HashIcon.svg';
|
||||
import GlobeIcon from '@assets/icons/GlobeIcon.svg';
|
||||
import LockIcon from '@assets/icons/LockIcon.svg';
|
||||
import FilterIcon from '@assets/icons/FilterIcon.svg';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
import { ChannelListItem } from '@raven/types/common/ChannelListItem';
|
||||
|
||||
export default function BrowseChannels() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
const { channels } = useGetChannels({ showArchived: true })
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [channelType, setChannelType] = useState('All')
|
||||
|
||||
// Filter channels based on search query
|
||||
const filteredChannels = channels.filter(channel =>
|
||||
channel.channel_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// Filter channels based on channel type
|
||||
const filteredChannelsByType = filteredChannels.filter(channel => {
|
||||
if (channelType === 'All') return true
|
||||
return channel.type === channelType
|
||||
})
|
||||
|
||||
const handleChannelPress = (channel: ChannelListItem) => {
|
||||
router.back()
|
||||
router.push(`../../chat/${channel.name}`)
|
||||
}
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Browse Channels',
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<CrossIcon color={colors.icon} height={24} width={24} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
}} />
|
||||
<View className="flex-1 flex-col gap-2 px-3 pt-3">
|
||||
<View className="flex flex-row items-center gap-2">
|
||||
<View className="flex-1 max-w-[90%]">
|
||||
<SearchInput
|
||||
onChangeText={setSearchQuery}
|
||||
value={searchQuery}
|
||||
/>
|
||||
</View>
|
||||
<ChannelFilter channel={channelType} setChannel={setChannelType} />
|
||||
</View>
|
||||
|
||||
<LegendList
|
||||
data={filteredChannelsByType}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
onPress={() => handleChannelPress(item)}
|
||||
className='flex flex-row gap-2 items-center p-2 rounded-lg ios:active:bg-linkColor'
|
||||
android_ripple={{ color: 'rgba(0,0,0,0.1)', borderless: false }}>
|
||||
<ChannelIcon type={item.type} fill={colors.icon} />
|
||||
<Text className="text-base">{item.channel_name}</Text>
|
||||
{item.is_archived ?
|
||||
<View className='px-1 mt-0.5 py-0.5 rounded-sm bg-red-100 dark:bg-red-900/40'>
|
||||
<Text className="text-[11px] text-red-700 dark:text-red-300">Archived</Text>
|
||||
</View> : null}
|
||||
</Pressable>
|
||||
)}
|
||||
keyExtractor={(item) => item.name}
|
||||
ListEmptyComponent={
|
||||
<View className="p-2">
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
No matching channels found for "{searchQuery}"
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
estimatedItemSize={44}
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
const ChannelFilter = ({ channel, setChannel }: { channel: string, setChannel: (channel: string) => void }) => {
|
||||
const { colors } = useColorScheme()
|
||||
return <DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className={`items-center p-2 border border-border rounded-lg w-fit ${channel !== 'All' ? 'border-primary bg-primary/5' : ''}`}>
|
||||
{
|
||||
channel === 'Public' ?
|
||||
<HashIcon fill={colors.icon} height={20} width={20} />
|
||||
: channel === 'Open' ?
|
||||
<GlobeIcon fill={colors.icon} height={20} width={20} />
|
||||
: channel === 'Private' ?
|
||||
<LockIcon fill={colors.icon} height={20} width={20} />
|
||||
: <FilterIcon color={colors.icon} height={20} width={20} />
|
||||
}
|
||||
</View>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item key="All" onSelect={() => setChannel('All')}>
|
||||
<DropdownMenu.ItemTitle>Any Channel</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
key="open"
|
||||
textValue="Open"
|
||||
onSelect={() => setChannel('Open')}>
|
||||
<Text>Open</Text>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
key="private"
|
||||
textValue="Private"
|
||||
onSelect={() => setChannel('Private')}>
|
||||
<Text>Private</Text>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
key="public"
|
||||
textValue="Public"
|
||||
onSelect={() => setChannel('Public')}>
|
||||
<Text>Public</Text>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import CreateChannelForm, { ChannelCreationForm } from '@components/features/channels/CreateChannel/CreateChannelForm';
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import CrossIcon from '@assets/icons/CrossIcon.svg';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useFrappeCreateDoc } from "frappe-react-sdk";
|
||||
import { ActivityIndicator } from '@components/nativewindui/ActivityIndicator';
|
||||
import { useGetCurrentWorkspace } from '@hooks/useGetCurrentWorkspace';
|
||||
import { toast } from 'sonner-native';
|
||||
import { useRouteToChannel } from '@hooks/useRouting';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { Platform } from 'react-native';
|
||||
export default function CreateChannel() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
const methods = useForm<ChannelCreationForm>({
|
||||
defaultValues: {
|
||||
type: 'Public',
|
||||
channel_name: '',
|
||||
channel_description: ''
|
||||
}
|
||||
})
|
||||
|
||||
const goToChannel = useRouteToChannel()
|
||||
const { workspace } = useGetCurrentWorkspace()
|
||||
|
||||
const { handleSubmit, reset: resetForm } = methods
|
||||
const { createDoc, error, loading: creatingChannel, reset: resetCreateHook } = useFrappeCreateDoc()
|
||||
|
||||
const reset = () => {
|
||||
resetCreateHook()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const onSubmit = async (data: ChannelCreationForm) => {
|
||||
return createDoc('Raven Channel', {
|
||||
...data,
|
||||
workspace: workspace
|
||||
}).then(result => {
|
||||
if (result) {
|
||||
toast.success("Channel created", result)
|
||||
// Navigate to channel
|
||||
goToChannel(result.name, 'replace')
|
||||
reset()
|
||||
resetForm()
|
||||
}
|
||||
}).catch(err => {
|
||||
toast.error("Failed to create channel", err)
|
||||
})
|
||||
}
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Add Channel',
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<CrossIcon color={colors.icon} height={24} width={24} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
headerRight() {
|
||||
return (
|
||||
<TouchableOpacity className="ios:px-0"
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
disabled={creatingChannel}>
|
||||
{creatingChannel ?
|
||||
<ActivityIndicator size="small" color={colors.primary} /> :
|
||||
<Text className="text-primary font-medium dark:text-secondary">Add</Text>}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
},
|
||||
}} />
|
||||
<FormProvider {...methods}>
|
||||
<CreateChannelForm />
|
||||
</FormProvider>
|
||||
</>
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Link, router, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import CrossIcon from '@assets/icons/CrossIcon.svg';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Platform, Pressable, View } from 'react-native';
|
||||
import SearchInput from '@components/common/SearchInput/SearchInput';
|
||||
import useGetDirectMessageChannels from '@raven/lib/hooks/useGetDirectMessageChannels';
|
||||
import { useContext, useState } from 'react';
|
||||
import { UserListContext } from '@raven/lib/providers/UserListProvider';
|
||||
import { useGetUser } from '@raven/lib/hooks/useGetUser';
|
||||
import { useFrappePostCall } from 'frappe-react-sdk';
|
||||
import { toast } from 'sonner-native';
|
||||
import UserAvatar from '@components/layout/UserAvatar';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import ErrorBanner from '@components/common/ErrorBanner';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
|
||||
export default function CreateDM() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
const { dmChannels } = useGetDirectMessageChannels()
|
||||
const { users } = useContext(UserListContext)
|
||||
const usersWithoutChannels = Array.from(users.values()).filter((user) => !dmChannels.find((channel) => channel.peer_user_id === user.name))
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const filteredUsers = usersWithoutChannels.filter((user) =>
|
||||
user.full_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Create DM',
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<CrossIcon color={colors.icon} height={24} width={24} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
}} />
|
||||
<View className="flex flex-col flex-1 gap-2 p-3">
|
||||
<View>
|
||||
<SearchInput
|
||||
onChangeText={setSearchQuery}
|
||||
value={searchQuery}
|
||||
/>
|
||||
</View>
|
||||
<LegendList
|
||||
data={filteredUsers}
|
||||
renderItem={({ item }) => <UserWithoutDMItem userID={item.name} />}
|
||||
keyExtractor={(item) => item.name}
|
||||
ListEmptyComponent={<EmptyState searchQuery={searchQuery} />}
|
||||
estimatedItemSize={60}
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
const UserWithoutDMItem = ({ userID }: { userID: string }) => {
|
||||
|
||||
const user = useGetUser(userID)
|
||||
const { call, error } = useFrappePostCall<{ message: string }>('raven.api.raven_channel.create_direct_message_channel')
|
||||
|
||||
const onSelect = () => {
|
||||
call({
|
||||
user_id: userID
|
||||
}).then((res) => {
|
||||
router.back()
|
||||
router.push(`../../chat/${res?.message}`)
|
||||
}).catch(err => {
|
||||
toast.error('Could not create a DM channel')
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{error ? <ErrorBanner error={error} /> : null}
|
||||
<Pressable
|
||||
onPress={onSelect}
|
||||
className='flex flex-row gap-2 items-center px-1.5 py-1.5 rounded-lg ios:active:bg-linkColor'
|
||||
android_ripple={{ color: 'rgba(0,0,0,0.1)', borderless: false }}>
|
||||
<UserAvatar
|
||||
src={user?.user_image}
|
||||
alt={user?.full_name ?? user?.name ?? ''}
|
||||
avatarProps={{ className: 'h-8 w-8' }}
|
||||
textProps={{ className: 'text-sm' }}
|
||||
isBot={user?.type === 'Bot'}
|
||||
/>
|
||||
<Text className='text-base'>{user?.full_name}</Text>
|
||||
{!user?.enabled ?
|
||||
<View className='px-1 mt-0.5 py-0.5 rounded-sm bg-red-100'>
|
||||
<Text className="text-[11px] text-red-700">Disabled</Text>
|
||||
</View>
|
||||
: null}
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const EmptyState = ({ searchQuery }: { searchQuery: string }) => {
|
||||
if (searchQuery.length > 0) {
|
||||
return (
|
||||
<Text className='p-2 text-sm text-muted-foreground'>
|
||||
No users found with '{searchQuery}' in their name
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text className='p-2text-sm text-muted-foreground'>
|
||||
There are no users that you do not already have a DM channel with
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { SafeAreaView, ScrollView, View } from 'react-native';
|
||||
import { ThemeToggle } from '@components/nativewindui/ThemeToggle';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import WorkspaceSwitcher from '@components/features/workspaces/WorkspaceSwitcher';
|
||||
import { useGetCurrentWorkspace } from '@hooks/useGetCurrentWorkspace';
|
||||
import { ViewSavedMessagesButton } from '@components/features/saved-messages/ViewSavedMessagesButton';
|
||||
import QuickSearchButton from '@components/features/search/QuickSearchButton';
|
||||
import AllChannelsList from '@components/features/channels/ChannelList/AllChannelsList';
|
||||
import { ViewMentionsButton } from '@components/features/mentions/ViewMentionsButton';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
export default function Home() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
const { workspace, switchWorkspace } = useGetCurrentWorkspace()
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.primary }}>
|
||||
<View style={{ backgroundColor: colors.primary }} className="flex flex-col px-4 pb-3 pt-2 gap-2 android:pt-12">
|
||||
<View className='flex-row items-center justify-between'>
|
||||
<WorkspaceSwitcher workspace={workspace} setWorkspace={switchWorkspace} />
|
||||
<View className='flex-row items-center gap-3'>
|
||||
<ViewMentionsButton />
|
||||
<ViewSavedMessagesButton />
|
||||
<ThemeToggle />
|
||||
</View>
|
||||
</View>
|
||||
<QuickSearchButton />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 5 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
className="rounded-t-[1.2rem]">
|
||||
<View className="flex flex-col">
|
||||
<AllChannelsList workspace={workspace} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Platform, Pressable, View } from 'react-native';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { FrappeConfig, FrappeContext, useSWRInfinite } from 'frappe-react-sdk';
|
||||
import { ActivityIndicator } from '@components/nativewindui/ActivityIndicator';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
import AtSignIcon from '@assets/icons/AtSignIcon.svg';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { RavenChannel } from '@raven/types/RavenChannelManagement/RavenChannel';
|
||||
import { RavenMessage } from '@raven/types/RavenMessaging/RavenMessage';
|
||||
import { getTimePassed } from '@raven/lib/utils/dateConversions';
|
||||
import { ChannelIcon } from '@components/features/channels/ChannelList/ChannelIcon';
|
||||
import { BaseMessageItem } from '@components/features/chat-stream/BaseMessageItem';
|
||||
import { Message } from '@raven/types/common/Message';
|
||||
import ChevronLeftIcon from '@assets/icons/ChevronLeftIcon.svg';
|
||||
import { useRouteToChannel } from '@hooks/useRouting';
|
||||
|
||||
interface MentionObject {
|
||||
/** ID of the message */
|
||||
name: string
|
||||
/** ID of the channel */
|
||||
channel_id: string
|
||||
/** Type of the channel */
|
||||
channel_type: RavenChannel['type']
|
||||
/** Name of the channel */
|
||||
channel_name: string
|
||||
/** Workspace name */
|
||||
workspace?: string
|
||||
/** Whether the channel is a thread */
|
||||
is_thread: 0 | 1
|
||||
/** Whether the channel is a direct message */
|
||||
is_direct_message: 0 | 1
|
||||
/** Date and time of the message */
|
||||
creation: string
|
||||
/** Type of the message */
|
||||
message_type: RavenMessage['message_type']
|
||||
/** Owner of the message */
|
||||
owner: string
|
||||
/** Text of the message */
|
||||
text: string
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export default function Mentions() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Mentions',
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<ChevronLeftIcon color={colors.icon} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
}} />
|
||||
<View className='flex-1 bg-background'>
|
||||
<MentionsList />
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
const MentionsList = () => {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
const { call } = useContext(FrappeContext) as FrappeConfig
|
||||
|
||||
const { data, size, isLoading, setSize } = useSWRInfinite<{ message: MentionObject[] }>(
|
||||
(pageIndex: number, previousPageData: { message: MentionObject[] } | null) => {
|
||||
if (previousPageData && !previousPageData.message.length) return null
|
||||
const start = pageIndex * PAGE_SIZE
|
||||
return ['raven.api.mentions.get_mentions', {
|
||||
limit: PAGE_SIZE,
|
||||
start
|
||||
}] as const
|
||||
},
|
||||
([endpoint, params]: readonly [string, { limit: number; start: number }]) => call.post(endpoint, params),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: true,
|
||||
revalidateOnMount: true
|
||||
}
|
||||
)
|
||||
|
||||
const isEmpty = data?.[0]?.message?.length === 0
|
||||
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined')
|
||||
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.message?.length < PAGE_SIZE)
|
||||
const mentions = data?.flatMap((page: { message: MentionObject[] }) => page.message) ?? []
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!isReachingEnd && !isLoadingMore) {
|
||||
setSize(size + 1)
|
||||
}
|
||||
}, [isReachingEnd, isLoadingMore, setSize, size])
|
||||
|
||||
if (isLoading) {
|
||||
return <View className="flex-1 justify-center items-center h-full">
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
}
|
||||
|
||||
return <LegendList
|
||||
data={mentions}
|
||||
ListEmptyComponent={<MentionsEmptyState />}
|
||||
renderItem={({ item }) => <MentionListItem message={item} />}
|
||||
keyExtractor={(item, index) => `${item.name}-${index}`}
|
||||
onEndReached={loadMore}
|
||||
estimatedItemSize={300}
|
||||
contentContainerStyle={{ paddingTop: 8, paddingBottom: 64, backgroundColor: colors.background }}
|
||||
/>
|
||||
}
|
||||
|
||||
const MentionListItem = ({ message }: { message: MentionObject }) => {
|
||||
|
||||
const goToChannel = useRouteToChannel()
|
||||
|
||||
const handleNavigateToChannel = () => {
|
||||
goToChannel(message.channel_id, 'push', message.is_thread === 0 ? 'Channel' : 'Thread')
|
||||
}
|
||||
|
||||
const channelName = useMemo(() => {
|
||||
|
||||
if (message.is_thread) {
|
||||
return `Thread`
|
||||
}
|
||||
|
||||
if (message.is_direct_message) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return message.channel_name
|
||||
}, [message])
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return <Pressable
|
||||
className='pb-2 rounded-md ios:active:bg-linkColor ios:active:dark:bg-linkColor'
|
||||
onPress={handleNavigateToChannel}>
|
||||
<View>
|
||||
<View className='flex flex-row items-center px-3 pt-2 gap-2'>
|
||||
<View className='flex flex-row items-center gap-1'>
|
||||
{message.is_thread === 0 && message.is_direct_message === 0 && <ChannelIcon type={message.channel_type} fill={colors.icon} size={16} />}
|
||||
<Text className='text-sm'>{channelName}</Text>
|
||||
</View>
|
||||
<Text className='text-[13px] text-muted'>|</Text>
|
||||
<TimeStamp creation={message.creation} />
|
||||
</View>
|
||||
<BaseMessageItem message={message as unknown as Message} />
|
||||
</View>
|
||||
</Pressable>
|
||||
}
|
||||
|
||||
const TimeStamp = ({ creation }: { creation: string }) => {
|
||||
return (
|
||||
<Text className="text-sm text-muted-foreground">{getTimePassed(creation)}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const MentionsEmptyState = () => {
|
||||
const { colors } = useColorScheme()
|
||||
return (
|
||||
<View className="flex flex-col p-4 gap-2 bg-background">
|
||||
<View className="flex flex-row items-center gap-2">
|
||||
<AtSignIcon color={colors.icon} height={19} width={19} />
|
||||
<Text className="text-foreground text-base font-medium">No mentions yet</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-foreground/60">
|
||||
When someone mentions you in a message, you'll see it here.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Link, router, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import CrossIcon from '@assets/icons/CrossIcon.svg';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { View, Platform } from 'react-native';
|
||||
import HashIcon from '@assets/icons/HashIcon.svg';
|
||||
import PlusIcon from '@assets/icons/PlusIcon.svg';
|
||||
import UserIcon from '@assets/icons/UserIcon.svg';
|
||||
import useGetChannels from '@raven/lib/hooks/useGetChannels';
|
||||
import useGetDirectMessageChannels from '@raven/lib/hooks/useGetDirectMessageChannels';
|
||||
import { useGetUser } from '@raven/lib/hooks/useGetUser';
|
||||
import { useState } from 'react';
|
||||
import SearchInput from '@components/common/SearchInput/SearchInput';
|
||||
import { useDebounce } from '@raven/lib/hooks/useDebounce';
|
||||
import { ActionButtonLarge } from '@components/common/Buttons/ActionButtonLarge';
|
||||
import { ChannelListItem, DMChannelListItem } from '@raven/types/common/ChannelListItem';
|
||||
import ChannelRowItem from '@components/common/CommonListItems/ChannelRowItem';
|
||||
import DMRowItem from '@components/common/CommonListItems/DMRowItem';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
|
||||
export default function QuickSearch() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
const openMenuItemSheet = (url: string) => {
|
||||
router.back()
|
||||
router.push(url, { relativeToDirectory: false })
|
||||
}
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const combinedList = useCombinedChannelAndDMList(searchQuery)
|
||||
|
||||
const onChannelPress = (channel: ChannelListItem | DMChannelListItem) => {
|
||||
router.back()
|
||||
router.push(`../../chat/${channel.name}`)
|
||||
}
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Quick Search',
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<CrossIcon color={colors.icon} height={24} width={24} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
}} />
|
||||
<View className="flex flex-col gap-3 p-3">
|
||||
<View>
|
||||
<SearchInput
|
||||
onChangeText={setSearchQuery}
|
||||
value={searchQuery}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
<View className='flex flex-row justify-center gap-2'>
|
||||
<ActionButtonLarge
|
||||
onPress={() => openMenuItemSheet('../home/browse-channels')}
|
||||
icon={<HashIcon fill={colors.grey} height={20} width={20} />}
|
||||
text="View Channels"
|
||||
textProps={{ className: 'text-sm text-muted-foreground' }}
|
||||
/>
|
||||
<ActionButtonLarge
|
||||
onPress={() => openMenuItemSheet('../home/create-dm')}
|
||||
icon={<UserIcon fill={colors.grey} height={20} width={20} />}
|
||||
text="Create DM"
|
||||
textProps={{ className: 'text-sm text-muted-foreground' }}
|
||||
/>
|
||||
<ActionButtonLarge
|
||||
onPress={() => openMenuItemSheet('../home/create-channel')}
|
||||
icon={<PlusIcon fill={colors.grey} height={20} width={20} />}
|
||||
text="New Channel"
|
||||
textProps={{ className: 'text-sm text-muted-foreground' }}
|
||||
/>
|
||||
</View>
|
||||
<LegendList
|
||||
data={combinedList}
|
||||
keyExtractor={(item) => item.name}
|
||||
estimatedItemSize={42}
|
||||
renderItem={({ item }) =>
|
||||
item.type === 'channel' ? (
|
||||
<ChannelRowItem key={item.name} channel={item as ChannelListItem} onPress={onChannelPress} />
|
||||
) : (
|
||||
<DMRowItem key={item.name} dmChannel={item as DMChannelListItem} onPress={onChannelPress} />
|
||||
)
|
||||
}
|
||||
contentContainerStyle={{ paddingBottom: 180 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<Text className="px-2 py-2 text-sm text-muted-foreground">No results found for "{searchQuery}"</Text>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines channels and DMs and filters them based on the search query
|
||||
* @param searchQuery - The search query to filter the channels and DMs by
|
||||
* @returns A list of channels and DMs that match the search query
|
||||
*/
|
||||
export const useCombinedChannelAndDMList = (searchQuery: string) => {
|
||||
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 200)
|
||||
const { channels } = useGetChannels({ showArchived: false })
|
||||
const { dmChannels } = useGetDirectMessageChannels()
|
||||
|
||||
// Filter channels
|
||||
const filteredChannels = channels
|
||||
.filter(channel => channel.channel_name.toLowerCase().includes(debouncedSearchQuery.toLowerCase()))
|
||||
.map(channel => ({ ...channel, type: 'channel' }))
|
||||
|
||||
// Filter DMs
|
||||
const filteredDms = dmChannels
|
||||
.map(dm => ({
|
||||
...dm,
|
||||
user: useGetUser(dm.peer_user_id),
|
||||
type: 'dm',
|
||||
}))
|
||||
.filter(dm => dm.user?.full_name?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()))
|
||||
|
||||
return [...filteredChannels, ...filteredDms]
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Platform, View } from 'react-native';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import BookMarkIcon from '@assets/icons/BookmarkIcon.svg';
|
||||
import { useFrappeGetCall } from 'frappe-react-sdk';
|
||||
import { Message } from '@raven/types/common/Message';
|
||||
import { ActivityIndicator } from '@components/nativewindui/ActivityIndicator';
|
||||
import SavedMessageItem from '@components/features/saved-messages/SavedMessageItem';
|
||||
import ChevronLeftIcon from '@assets/icons/ChevronLeftIcon.svg';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
import ErrorBanner from '@components/common/ErrorBanner';
|
||||
|
||||
export default function SavedMessages() {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return <>
|
||||
<Stack.Screen options={{
|
||||
title: 'Saved Messages',
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
headerLeft: Platform.OS === 'ios' ? () => {
|
||||
return (
|
||||
<Link asChild href="../" relativeToDirectory>
|
||||
<Button variant="plain" className="ios:px-0" hitSlop={10}>
|
||||
<ChevronLeftIcon color={colors.icon} />
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
} : undefined,
|
||||
}} />
|
||||
<View className='flex-1 bg-background'>
|
||||
<SavedMessagesContent />
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
const SavedMessagesContent = () => {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
const { data, isLoading, error } = useFrappeGetCall<{ message: (Message & { workspace?: string })[] }>("raven.api.raven_message.get_saved_messages", undefined, undefined, {
|
||||
revalidateOnFocus: false
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <View className="flex-1 justify-center items-center h-full">
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
}
|
||||
|
||||
if (!data && error) {
|
||||
return (
|
||||
<View className='p-4'>
|
||||
<ErrorBanner error={error} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return <LegendList
|
||||
data={data?.message ?? []}
|
||||
ListEmptyComponent={<SavedMessagesEmptyState />}
|
||||
renderItem={({ item }) => <SavedMessageItem message={item} />}
|
||||
keyExtractor={(item) => item.name}
|
||||
contentContainerStyle={{ paddingTop: 8, backgroundColor: colors.background }}
|
||||
/>
|
||||
}
|
||||
|
||||
const SavedMessagesEmptyState = () => {
|
||||
const { colors } = useColorScheme()
|
||||
return (
|
||||
<View className="flex flex-col p-4 gap-2 bg-background">
|
||||
<View className="flex flex-row items-center gap-2">
|
||||
<BookMarkIcon fill={colors.icon} height={20} width={20} />
|
||||
<Text className="text-foreground text-base font-medium">Your saved messages will appear here</Text>
|
||||
</View>
|
||||
<Text className="text-sm text-foreground/60">
|
||||
Saved messages are a convenient way to keep track of important information or messages you want to refer back to later.
|
||||
</Text>
|
||||
<Text className="text-sm text-foreground/60">
|
||||
You can save messages by simply clicking on the bookmark icon in message actions.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
const ProfileLayout = () => {
|
||||
|
||||
const { colors, isDarkColorScheme } = useColorScheme()
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{
|
||||
headerShadowVisible: false,
|
||||
headerStyle: { backgroundColor: isDarkColorScheme ? colors.background : colors.card }
|
||||
}}>
|
||||
<Stack.Screen name='index'
|
||||
options={{
|
||||
title: 'Profile',
|
||||
headerShadowVisible: true,
|
||||
contentStyle: { backgroundColor: isDarkColorScheme ? colors.background : colors.card }
|
||||
}} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileLayout
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { router, Stack } from 'expo-router';
|
||||
import { View } from 'react-native';
|
||||
import { useFrappePostCall } from 'frappe-react-sdk';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import { Form, FormItem, FormSection } from '@components/nativewindui/Form';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { TextField } from '@components/nativewindui/TextField';
|
||||
import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser';
|
||||
import { toast } from 'sonner-native';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { ActivityIndicator } from '@components/nativewindui/ActivityIndicator';
|
||||
import HeaderBackButton from '@components/common/Buttons/HeaderBackButton';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
export default function CustomStatusScreen() {
|
||||
|
||||
const { myProfile, mutate } = useCurrentRavenUser()
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
const [customStatus, setCustomStatus] = useState(myProfile?.custom_status ?? '')
|
||||
|
||||
const { call, loading } = useFrappePostCall('raven.api.raven_users.update_raven_user')
|
||||
|
||||
const handleCustomStatusUpdate = async () => {
|
||||
call({
|
||||
custom_status: customStatus
|
||||
}).then(() => {
|
||||
toast.success("Status updated")
|
||||
mutate()
|
||||
router.back()
|
||||
}).catch(() => {
|
||||
toast.error("Failed to update status")
|
||||
})
|
||||
}
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerLeft() {
|
||||
return (
|
||||
<HeaderBackButton />
|
||||
)
|
||||
},
|
||||
headerTitle: () => <Text className='ml-2 text-base font-semibold'>Custom Status</Text>,
|
||||
headerRight() {
|
||||
return (
|
||||
<Button variant="plain" className="ios:px-0"
|
||||
onPress={handleCustomStatusUpdate}
|
||||
disabled={loading}>
|
||||
{loading ?
|
||||
<ActivityIndicator size="small" color={colors.primary} /> :
|
||||
<Text className="text-primary dark:text-secondary">Save</Text>}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
}} />
|
||||
<KeyboardAwareScrollView
|
||||
bottomOffset={8}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}>
|
||||
<Form className="gap-5 px-4 pt-8">
|
||||
<FormSection footnote="Share what you are up to.">
|
||||
<FormItem>
|
||||
<TextField
|
||||
autoFocus
|
||||
className="pl-0.5"
|
||||
leftView={
|
||||
<View className="w-36 justify-between flex-row items-center pl-2">
|
||||
<Text className="font-medium">Custom Status</Text>
|
||||
</View>
|
||||
}
|
||||
placeholder="e.g. Out of Office"
|
||||
value={customStatus}
|
||||
onChangeText={setCustomStatus}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormSection>
|
||||
</Form>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { router, Stack } from 'expo-router';
|
||||
import { View } from 'react-native';
|
||||
import { useFrappeUpdateDoc } from 'frappe-react-sdk';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button } from '@components/nativewindui/Button';
|
||||
import { Form, FormItem, FormSection } from '@components/nativewindui/Form';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { TextField } from '@components/nativewindui/TextField';
|
||||
import useCurrentRavenUser from '@raven/lib/hooks/useCurrentRavenUser';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { ActivityIndicator } from '@components/nativewindui/ActivityIndicator';
|
||||
import { toast } from 'sonner-native';
|
||||
import HeaderBackButton from '@components/common/Buttons/HeaderBackButton';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
export default function FullNameScreen() {
|
||||
|
||||
const { myProfile, mutate } = useCurrentRavenUser()
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
const [fullName, setFullName] = useState(myProfile?.full_name ?? '')
|
||||
|
||||
const { updateDoc, loading } = useFrappeUpdateDoc()
|
||||
|
||||
const handleFullNameUpdate = async () => {
|
||||
return updateDoc("Raven User", myProfile?.name ?? '', {
|
||||
full_name: fullName,
|
||||
}).then(() => {
|
||||
toast.success("User name updated")
|
||||
mutate()
|
||||
router.back();
|
||||
}).catch(() => {
|
||||
toast.error("Failed to update user name")
|
||||
})
|
||||
}
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerLeft() {
|
||||
return (
|
||||
<HeaderBackButton />
|
||||
)
|
||||
},
|
||||
headerTitle: () => <Text className='ml-2 text-base font-semibold'>User Name</Text>,
|
||||
headerRight() {
|
||||
return (
|
||||
<Button variant="plain" className="ios:px-0"
|
||||
onPress={handleFullNameUpdate}
|
||||
disabled={loading || !fullName.length}>
|
||||
{loading ?
|
||||
<ActivityIndicator size="small" color={colors.primary} /> :
|
||||
<Text className="text-primary dark:text-secondary">Save</Text>}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
headerStyle: { backgroundColor: colors.background }
|
||||
}} />
|
||||
<KeyboardAwareScrollView
|
||||
bottomOffset={8}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}>
|
||||
<Form className="gap-5 px-4 pt-8">
|
||||
<FormSection footnote="This name will be used to identify you in the app.">
|
||||
<FormItem>
|
||||
<TextField
|
||||
autoFocus
|
||||
className="pl-0.5"
|
||||
leftView={
|
||||
<View className="w-36 justify-between flex-row items-center pl-2">
|
||||
<Text className="font-medium">Full Name</Text>
|
||||
</View>
|
||||
}
|
||||
placeholder="Full Name"
|
||||
value={fullName}
|
||||
onChangeText={setFullName}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormSection>
|
||||
</Form>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Platform, ScrollView, View } from 'react-native';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import LogOutButton from '@components/features/profile/profile-settings/LogOutButton';
|
||||
import NotificationSetting from '@components/features/profile/profile-settings/NotificationSetting';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import AppearanceSetting from '@components/features/profile/profile-settings/AppearanceSetting';
|
||||
import UserAvailability from '@components/features/profile/profile-settings/UserAvailability';
|
||||
import UserFullName from '@components/features/profile/profile-settings/UserFullName';
|
||||
import CustomStatus from '@components/features/profile/profile-settings/CustomStatus';
|
||||
import ProfilePicture from '@components/features/profile/upload-profile/ProfilePicture';
|
||||
import { nativeApplicationVersion, nativeBuildVersion } from 'expo-application';
|
||||
import Preferences from '@components/features/profile/profile-settings/Preferences';
|
||||
import SwitchSitesSetting from '@components/features/profile/profile-settings/SwitchSitesSetting';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
const SCREEN_OPTIONS = {
|
||||
title: 'Profile',
|
||||
headerTransparent: Platform.OS === 'ios',
|
||||
headerBlurEffect: 'systemMaterial',
|
||||
} as const
|
||||
|
||||
export default function Profile() {
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={SCREEN_OPTIONS} />
|
||||
<View className='flex-1 px-4'>
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}>
|
||||
<View className='flex flex-col gap-4 mt-1.5'>
|
||||
<ProfilePicture />
|
||||
<View className='flex flex-col gap-0.5'>
|
||||
<Text className='pl-2 pb-1 text-xs text-muted-foreground/80'>Personal Info</Text>
|
||||
<UserFullName />
|
||||
<CustomStatus />
|
||||
<UserAvailability />
|
||||
</View>
|
||||
<View className='flex flex-col gap-0.5'>
|
||||
<Text className='pl-2 pb-1 text-xs text-muted-foreground/80'>Preferences</Text>
|
||||
<NotificationSetting />
|
||||
<AppearanceSetting />
|
||||
<Preferences />
|
||||
<SwitchSitesSetting />
|
||||
</View>
|
||||
<LogOutButton />
|
||||
<View className='flex flex-col justify-center items-center pt-2 gap-1'>
|
||||
<Text className='text-lg text-muted-foreground/90 font-cal-sans'>raven</Text>
|
||||
<View className='flex flex-col items-center justify-center'>
|
||||
<Text className='text-xs text-muted-foreground/80'>by The Commit Company</Text>
|
||||
<Text className='text-xs text-muted-foreground/80'>Version {nativeApplicationVersion} ({nativeBuildVersion})</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,144 +0,0 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Form, FormItem, FormSection } from '@components/nativewindui/Form';
|
||||
import { Text } from '@components/nativewindui/Text';
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import HeaderBackButton from '@components/common/Buttons/HeaderBackButton';
|
||||
import { useAtom } from 'jotai';
|
||||
import { doubleTapMessageEmojiAtom, quickReactionEmojisAtom } from '@lib/preferences';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import EmojiPicker from '@components/common/EmojiPicker/EmojiPicker';
|
||||
import { useState } from 'react';
|
||||
import { Sheet, useSheetRef } from '@components/nativewindui/Sheet';
|
||||
import { BottomSheetView } from '@gorhom/bottom-sheet';
|
||||
import { Emoji } from '@components/common/EmojiPicker/Picker';
|
||||
import CommonErrorBoundary from '@components/common/CommonErrorBoundary';
|
||||
|
||||
const REACTION_PRESSABLE_STYLES = 'w-12 h-12 flex items-center justify-center p-2 bg-card dark:bg-card rounded-full active:bg-muted/20'
|
||||
|
||||
export default function PreferencesScreen() {
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
const [quickReactionEmojis, setQuickReactionEmojis] = useAtom(quickReactionEmojisAtom)
|
||||
|
||||
const [doubleTapMessageEmoji, setDoubleTapMessageEmoji] = useAtom(doubleTapMessageEmojiAtom)
|
||||
|
||||
const emojiBottomSheetRef = useSheetRef()
|
||||
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState<false | 'doubleTapMessageEmoji' | 'quickReactionEmojis-1' | 'quickReactionEmojis-2' | 'quickReactionEmojis-3' | 'quickReactionEmojis-4' | 'quickReactionEmojis-5'>(false)
|
||||
|
||||
|
||||
const onEmojiSelect = (emoji: Emoji) => {
|
||||
if (isEmojiPickerOpen === 'doubleTapMessageEmoji') {
|
||||
setDoubleTapMessageEmoji(emoji.native)
|
||||
} else if (isEmojiPickerOpen && isEmojiPickerOpen.includes('quickReactionEmojis')) {
|
||||
const index = parseInt(isEmojiPickerOpen.split('-')[1]) - 1
|
||||
const newEmojis = [...quickReactionEmojis]
|
||||
newEmojis[index] = emoji.native
|
||||
setQuickReactionEmojis(newEmojis)
|
||||
}
|
||||
|
||||
setIsEmojiPickerOpen(false)
|
||||
emojiBottomSheetRef.current?.dismiss()
|
||||
}
|
||||
|
||||
const openEmojiPicker = (type: 'doubleTapMessageEmoji' | 'quickReactionEmojis-1' | 'quickReactionEmojis-2' | 'quickReactionEmojis-3' | 'quickReactionEmojis-4' | 'quickReactionEmojis-5') => {
|
||||
setIsEmojiPickerOpen(type)
|
||||
emojiBottomSheetRef.current?.present()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerLeft() {
|
||||
return (
|
||||
<HeaderBackButton />
|
||||
)
|
||||
},
|
||||
headerTitle: () => <Text className='ml-2 text-base font-semibold'>Preferences</Text>,
|
||||
headerStyle: { backgroundColor: colors.background },
|
||||
}} />
|
||||
<KeyboardAwareScrollView
|
||||
bottomOffset={8}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}>
|
||||
<Form className="gap-5 px-4 pt-8">
|
||||
<FormSection footnote="Set your preferred emoji for reactions on double tap.">
|
||||
<FormItem className='py-0.5'>
|
||||
<Pressable onPress={() => openEmojiPicker('doubleTapMessageEmoji')}
|
||||
hitSlop={10}
|
||||
className='bg-card dark:bg-card rounded-xl active:bg-muted/20'>
|
||||
<View className='flex flex-row py-2.5 px-4 justify-between'>
|
||||
<View className='flex-row items-center gap-2'>
|
||||
<Text className='text-base'>React on Double Tap with</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className='text-base text-muted-foreground/80'>{doubleTapMessageEmoji}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</FormItem>
|
||||
</FormSection>
|
||||
|
||||
<FormSection footnote="Tap an emoji to set it as your preferred reaction.">
|
||||
<FormItem className='py-0.5'>
|
||||
<View className='flex flex-col gap-2 px-2'>
|
||||
<Text className='text-base font-medium'>Preferred Emojis for Reactions</Text>
|
||||
<View className='flex flex-row gap-2 justify-around'>
|
||||
<Pressable
|
||||
onPress={() => openEmojiPicker('quickReactionEmojis-1')}
|
||||
hitSlop={10}
|
||||
className={REACTION_PRESSABLE_STYLES}>
|
||||
<Text className='text-xl'>{quickReactionEmojis[0]}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => openEmojiPicker('quickReactionEmojis-2')}
|
||||
hitSlop={10}
|
||||
className={REACTION_PRESSABLE_STYLES}>
|
||||
<Text className='text-xl'>{quickReactionEmojis[1]}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => openEmojiPicker('quickReactionEmojis-3')}
|
||||
hitSlop={10}
|
||||
className={REACTION_PRESSABLE_STYLES}>
|
||||
<Text className='text-xl'>{quickReactionEmojis[2]}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => openEmojiPicker('quickReactionEmojis-4')}
|
||||
hitSlop={10}
|
||||
className={REACTION_PRESSABLE_STYLES}>
|
||||
<Text className='text-xl'>{quickReactionEmojis[3]}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => openEmojiPicker('quickReactionEmojis-5')}
|
||||
hitSlop={10}
|
||||
className={REACTION_PRESSABLE_STYLES}>
|
||||
<Text className='text-xl'>{quickReactionEmojis[4]}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</FormItem>
|
||||
</FormSection>
|
||||
</Form>
|
||||
<Sheet enableDynamicSizing={false} ref={emojiBottomSheetRef} snapPoints={["80"]}>
|
||||
<BottomSheetView className='flex-1'>
|
||||
<EmojiPicker allowCustomEmojis={false} onReact={onEmojiSelect} />
|
||||
</BottomSheetView>
|
||||
</Sheet>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorBoundary = CommonErrorBoundary
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useColorScheme } from '@hooks/useColorScheme';
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
const ThreadsLayout = () => {
|
||||
|
||||
const { colors } = useColorScheme()
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerStyle: { backgroundColor: colors.background } }}>
|
||||
<Stack.Screen name='index'
|
||||
options={{
|
||||
title: 'Threads',
|
||||
headerLargeTitle: true
|
||||
}} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThreadsLayout
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user