Compare commits

..
498 Commits
Author SHA1 Message Date
Danny Avila cf2bab31cf reset packages and uninstall unused dep. 2023-03-29 14:12:24 -04:00
Danny Avila d6fdf41011 fix: add text to global state 2023-03-29 11:25:27 -04:00
Danny AvilaandGitHub 79bb54db9c Merge pull request #140 from danny-avila/feat-refactor
Code refactoring
2023-03-29 11:00:04 -04:00
Danny Avila 4a94ee7af8 feat: allow default gpt api model before frontend customization 2023-03-29 10:17:24 -04:00
Danny Avila 39ff9c1bc2 fix: resize bing tabs 2023-03-29 09:02:49 -04:00
Danny Avila b9699feb3b fix: prevent scroll to top on initial messages 2023-03-29 08:49:39 -04:00
Danny Avila f93df2aea6 lift react markdown 2023-03-29 08:19:00 -04:00
Wentao Lyu 2c1871d5ba fix: update url rule in nginx 2023-03-29 16:11:43 +08:00
Wentao Lyu e796a19136 fix: remove related messages when deleting conversations. 2023-03-29 13:32:01 +08:00
Daniel Avila 0d7300be9b fix: chatgptBrowser handling and ask.js refactor 2023-03-28 22:28:43 -04:00
Daniel Avila 005d8fb178 edit titleConvo for consistent results 2023-03-28 19:50:37 -04:00
Daniel Avila f53b620df5 chore: add back clear convo dialog 2023-03-28 19:10:22 -04:00
Wentao Lyu e706f0ea9e typo 2023-03-29 06:44:18 +08:00
Wentao Lyu e663270072 fix: show message list. 2023-03-29 05:48:44 +08:00
Danny Avila 74924d2eea reset package-lock again 2023-03-28 16:29:42 -04:00
Danny Avila a04ea81143 Merge branch 'feat-refactor' of https://github.com/danny-avila/chatgpt-clone into feat-refactor 2023-03-28 15:35:40 -04:00
Wentao Lyu e818ee913d fix: add typescript package for react 2023-03-29 03:34:13 +08:00
Danny Avila 2a16a64612 fix: cursor appears on placeholder message 2023-03-28 15:32:22 -04:00
Danny Avila 95e9f05688 chore: reset package lock file 2023-03-28 15:15:10 -04:00
Wentao Lyu ee3f6e1d1d remove react-redux.
help needed, please generate a package-lock.json
2023-03-29 02:42:37 +08:00
Wentao Lyu b7af3595cf cleanup remove redux store 2023-03-29 02:38:24 +08:00
Wentao Lyu aa26eea8c5 fix: missing icon of search result
feat: use search result message as single list
2023-03-29 02:29:15 +08:00
Danny AvilaandGitHub c4be973b78 Update README.md 2023-03-28 14:12:08 -04:00
Wentao Lyu dc743df255 feat: update title generator prompt, to support better on language. 2023-03-29 01:50:57 +08:00
Wentao Lyu 319e4f0f95 fix: set to default model in searchPlaceholderConversation
fix: set max auth cookie to 7 days
2023-03-29 01:26:58 +08:00
Wentao Lyu f595cb2aa1 fix: three dot looks wide on android chrome. 2023-03-29 01:09:03 +08:00
Wentao Lyu 894aad9f0b sync with main 2023-03-29 00:20:57 +08:00
Wentao Lyu 5467d550e5 Merge remote-tracking branch 'origin/main' into feat-refactor-0.1.1 2023-03-29 00:19:46 +08:00
Wentao Lyu d0d0a3d23e feat: print nothing found when no search result.
fix: handle 404 of conversation fetch failed
2023-03-29 00:18:27 +08:00
Wentao Lyu 370dc2dd8a feat: support search-style-url
fix: url can be null in conversationId and query
fix: get conversation api should handle not found.
2023-03-29 00:08:02 +08:00
Danny AvilaandGitHub d9363b93c6 Create .github/FUNDING.yml 2023-03-28 11:48:47 -04:00
Danny AvilaandGitHub b1b904ce5a Merge pull request #139 from danny-avila/fix-bugs
Fix bugs
2023-03-28 11:45:05 -04:00
Danny Avila 4564b648f7 fix: failsafe to prevent all convos clearing from delete button 2023-03-28 11:42:36 -04:00
Danny Avila 0fbbe74479 revert bing to last working state 2023-03-28 11:38:56 -04:00
Wentao Lyu 8ea98cca5d refactor: bing button
THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 23:00:29 +08:00
Wentao Lyu c7c30d8bb5 refactor: basic message and send message. as well as model
THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 22:39:27 +08:00
Wentao Lyu de8f519742 refactor: update package-lock 2023-03-28 20:37:34 +08:00
Wentao Lyu af3d74b104 refactor: nav and search.
feat: use recoil to replace redux
feat: use react-native

THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 20:36:21 +08:00
Danny AvilaandGitHub 2ad675196f Merge pull request #137 from danny-avila/fix-css
fix: set max height on customgpt prompt prefix
2023-03-28 08:11:43 -04:00
Danny Avila e434b3afea fix: set max height on customgpt prompt prefix 2023-03-28 08:10:22 -04:00
Wentao Lyu d8ccc5b870 fix: clearConvo will remove all messages 2023-03-28 01:19:44 +08:00
Wentao Lyu 7d43032a98 feat: return home page on any path
fix: clearConvo will remove all messages
2023-03-28 00:15:29 +08:00
Danny AvilaandGitHub 5b8483828b Merge pull request #136 from danny-avila/fix-css
fix: css matches official more closely with new markdown handling
2023-03-27 09:56:14 -04:00
Danny Avila 3c23f16b98 fix: css matches official more closely with new markdown handling 2023-03-27 09:55:24 -04:00
Danny AvilaandGitHub 7dc479e0a0 Merge pull request #135 from HyunggyuJang/follow-up/title-generation
updateConvo for title updating
2023-03-27 09:31:45 -04:00
Danny AvilaandGitHub ee9419cb0b Merge pull request #133 from HyunggyuJang/refactor/markdown
Use react markdown as default, cleanup dependencies
2023-03-27 09:31:32 -04:00
Danny AvilaandGitHub 1c26bbe43e Merge pull request #128 from HyunggyuJang/sydney/tone-adjustment
Make tone adjustment available after conversation started if sydney
2023-03-27 09:31:16 -04:00
Danny AvilaandGitHub 52d57f67aa Merge pull request #127 from HyunggyuJang/bing/fast-tone
Add fast tab for bing tone
2023-03-27 09:31:03 -04:00
Hyunggyu Jang 7486a56816 updateConvo for title updating 2023-03-27 12:53:40 +09:00
Hyunggyu Jang 40e23b013a Use react markdown as default, cleanup dependencies 2023-03-27 10:22:37 +09:00
Hyunggyu Jang c119c4044a Adjust color 2023-03-26 19:52:47 +09:00
Hyunggyu Jang 6f037309ad Fix for mobile view 2023-03-26 13:49:55 +09:00
Hyunggyu Jang af4110ff15 Do not allow to override bing AI tone in the middle of conversation 2023-03-26 13:25:29 +09:00
Hyunggyu Jang 55f04ffa60 Make tone adjustment available after conversation started if sydney 2023-03-26 13:12:48 +09:00
Hyunggyu Jang e5cf51b2d6 Add fast tab for bing tone 2023-03-26 13:10:36 +09:00
Danny AvilaandGitHub e8e512a451 Merge pull request #126 from danny-avila/minor-updates
chore: minor updates
2023-03-25 10:21:41 -04:00
Daniel Avila c2967eafa4 chore: update docker-compose for interchangeable env for search, uninstalled unused deps 2023-03-25 10:20:39 -04:00
Danny AvilaandGitHub 853b4dfd49 Merge pull request #125 from danny-avila/upgrade-node
Upgrade node
2023-03-25 10:02:17 -04:00
Danny AvilaandGitHub e49adaa314 Merge pull request #124 from danny-avila/bing-styles
Bing styles
2023-03-25 10:01:19 -04:00
Daniel Avila 394cdcd9f4 fix: remove 'class=' error 2023-03-25 09:55:33 -04:00
Daniel Avila c5561434c8 lift node-api library 2023-03-25 09:53:39 -04:00
Daniel Avila 26e7a715e0 feat: complete bing styles (bing is passed tone style) 2023-03-25 09:40:36 -04:00
Daniel Avila b07b74ba54 feat: complete bing styles (view) 2023-03-25 09:34:00 -04:00
Danny AvilaandGitHub 70152174b9 Merge pull request #123 from HyunggyuJang/fix/unknown-lang
Fix Unknown language bug
2023-03-25 08:39:10 -04:00
Hyunggyu Jang 8bd29f6d98 Fix Unknown language bug 2023-03-25 18:40:12 +09:00
Daniel Avila b8720eec3d feat: dark mode for style tabs 2023-03-24 19:02:42 -04:00
Daniel Avila ad0f2408c8 Merge branch 'main' into bing-styles 2023-03-24 18:50:56 -04:00
Danny AvilaandGitHub e2ad9accbe Merge pull request #122 from danny-avila/fix-search
Fix search
2023-03-24 18:45:03 -04:00
Daniel Avila b496174b4c fix: correctly escapes user content in search results 2023-03-24 18:44:12 -04:00
Daniel Avila 3295eb806c chore: expose mongodb on port 27018 2023-03-24 18:27:51 -04:00
Daniel Avila 34bef48e84 fix: correctly searches bing messages 2023-03-24 18:26:59 -04:00
Danny Avila a6c93ad681 dark mode in progress 2023-03-24 16:31:27 -04:00
Danny Avila 89ab74a913 feat: complete frontend/backend tone handling 2023-03-24 16:21:10 -04:00
Danny AvilaandGitHub a46ec62532 Update README.md 2023-03-24 16:19:42 -04:00
Danny AvilaandGitHub 22a967927f Update README.md 2023-03-24 16:19:16 -04:00
Danny AvilaandGitHub 0aa7581358 Add files via upload
in-depth local instructions thanks to @fuegovic
2023-03-24 16:17:04 -04:00
Danny AvilaandGitHub 513cd28528 Update README.md 2023-03-24 16:16:06 -04:00
Danny Avila 83b88bd759 feat: complete view for styles 2023-03-24 14:46:07 -04:00
Danny AvilaandGitHub 4f18a471b0 Merge pull request #121 from danny-avila/fix-clear-mobile
fix: clear convos on mobile now has top z-index
2023-03-24 08:20:30 -04:00
Danny AvilaandGitHub 737abd54ac Merge pull request #120 from HyunggyuJang/refactor/sydney
Refactor sydney & fix error
2023-03-24 08:19:06 -04:00
Hyunggyu Jang 1de51467ec Fix forgetfulness for bing/sydney 2023-03-24 12:48:20 +09:00
Hyunggyu Jang dcc0ab98e1 refactor: Remove verbose user message update for sydney 2023-03-24 12:27:46 +09:00
Daniel Avila 730256dcda fix: clear convos on mobile now has top z-index 2023-03-23 23:18:36 -04:00
Danny AvilaandGitHub 40e793477b Update README.md 2023-03-23 16:30:58 -04:00
Danny AvilaandGitHub b856db4772 Update .env.example
fix typo
2023-03-23 16:26:22 -04:00
Danny AvilaandGitHub cab1cbceab Merge pull request #118 from danny-avila/fix-auth
fix: auth env var must have no value, as well as assigned sample_user in route
2023-03-23 15:41:58 -04:00
Danny Avila b73be0dcfa fix: auth env var must have no value, as well as assigned username incase a falsy value is set 2023-03-23 15:37:25 -04:00
Danny AvilaandGitHub c6fb3018e7 Merge pull request #117 from danny-avila/search-final
Search final
2023-03-23 13:37:55 -04:00
Danny Avila 95cf27ee3e edit env example 2023-03-23 13:30:55 -04:00
Danny Avila 7afe09fa02 chore: clear timeouts 2023-03-23 13:16:07 -04:00
Danny Avila bff33c79b3 merge latest pr from main 2023-03-23 13:04:40 -04:00
Danny Avila f73936e5f4 Merge branch 'main' into search-final 2023-03-23 13:02:52 -04:00
Danny Avila 2b8d37c38f edit env example 2023-03-23 12:45:42 -04:00
Danny Avila ca3da2505a edit env example 2023-03-23 11:49:54 -04:00
Danny AvilaandGitHub d3046dca07 Merge pull request #115 from HyunggyuJang/sydney/branching
Enable branching (edit message) for Sydney
2023-03-23 11:38:44 -04:00
Danny AvilaandGitHub 25fd39c2b9 Update README.md
organize readme for readability
2023-03-23 11:35:23 -04:00
Daniel Avila ab8724b568 edit docker-compose for meilisearch 2023-03-22 22:35:25 -04:00
Hyunggyu Jang c240d14864 Enable branching (edit message) for Sydney 2023-03-23 11:27:14 +09:00
Daniel Avila 1d464fdcfa reduce noisy meili errors if not set 2023-03-22 21:23:01 -04:00
Daniel Avila aacc292522 Merge branch 'main' into search-final 2023-03-22 20:57:20 -04:00
Danny AvilaandGitHub 350a1bbae0 Update README.md 2023-03-22 20:48:47 -04:00
Danny AvilaandGitHub 90b74aff2e Merge pull request #104 from HyunggyuJang/refactor/docker
Refactor: merge docker setup file into one dockerfile & one docker-compose.yml
2023-03-22 20:13:10 -04:00
Daniel Avila 37f36ec44a chore: update meilisearch compose 2023-03-22 20:12:38 -04:00
Daniel Avila 71b7eaa3f5 chore: update env example and add browser client config 2023-03-22 20:09:52 -04:00
Daniel Avila 719413f87a feat: syncs across document deletions 2023-03-22 19:53:09 -04:00
Daniel Avila 97634865eb feat: syncs across document deletions 2023-03-22 19:52:38 -04:00
Daniel Avila 1dbfb0dab7 minor styling changes, cache queried messages on server 2023-03-22 18:26:29 -04:00
Daniel Avila 0a671849b5 feat: clearing convos requires confirmation 2023-03-22 17:51:51 -04:00
Daniel Avila 655e7ce6d6 chore: improve meili error handling 2023-03-22 17:15:32 -04:00
Danny Avila 68979015c1 markdown styling changes in progress 2023-03-22 16:31:57 -04:00
Danny Avila 8f58c95452 feat: main styling/ui/ux final changes 2023-03-22 16:06:11 -04:00
Danny Avila 67161c983f chore: meilisearch setup config 2023-03-22 10:23:55 -04:00
Danny Avila 73449d9ec6 feat: build tree by convoId 2023-03-22 10:23:36 -04:00
HyunggyuJangandGitHub f5d102b7bd Update docker-compose.yml 2023-03-22 23:20:03 +09:00
Hyunggyu Jang 40ed6fa9ec Provide nginx docker build recipe 2023-03-22 22:59:29 +09:00
Danny Avila 5164cf46ac chore: error handling for complete omission of env var 2023-03-22 09:38:38 -04:00
Daniel Avila 277685c218 search: updating search endpoint (wip) 2023-03-22 01:34:36 -04:00
Daniel Avila e25aa74d7b search: correctly register/export schema/models, also made IIFE 2023-03-22 01:33:49 -04:00
Daniel Avila 47a6cfcafd chore: error controller (wip) 2023-03-22 01:32:55 -04:00
Daniel Avila 83a96706b4 chore: add prettier 2023-03-22 01:32:30 -04:00
Daniel Avila 75be4d9722 search: helper fn for invalid convoId strings 2023-03-22 01:31:49 -04:00
Daniel Avila a5cf2f9148 search: correctly register/export schema/models for mongoMeili 2023-03-22 01:31:01 -04:00
Daniel Avila 8be19f9982 search: sync on offset between meili and mongo 2023-03-22 01:30:04 -04:00
Hyunggyu Jang 36f3d37ecc Remove nginx setting 2023-03-22 10:50:59 +09:00
Hyunggyu Jang c233cc0d5c Move Dockerfiles into one toplevel Dockerfile 2023-03-22 10:50:44 +09:00
Daniel Avila 194051e424 feat: api will plugin mongoMeili without a valid connection 2023-03-21 20:06:27 -04:00
Daniel Avila 94c0fbb525 feat: api will disable search if no meilisearch connection 2023-03-21 19:44:31 -04:00
Daniel Avila 97a6cd801b feat: simple api call to enable search 2023-03-21 19:31:57 -04:00
Danny AvilaandGitHub 1041146fcb Merge pull request #108 from danny-avila/upgrade-fix
fix: chatgpt now using latest
2023-03-21 17:41:05 -04:00
Danny Avila e531a17e0f fix: chatgpt now using latest 2023-03-21 16:32:32 -04:00
Danny AvilaandGitHub 30a7a80bfc Merge pull request #107 from danny-avila/chores
Chores
2023-03-21 14:27:00 -04:00
Danny Avila 67f8374c9e chore: eslint rules and blinker update in text handling 2023-03-21 13:41:31 -04:00
Danny Avila 0cc4aea204 chore: reorg. content files, add blinking cursor 2023-03-21 09:46:08 -04:00
Danny Avila 04796824d5 chore: init eslint 2023-03-21 08:48:35 -04:00
Danny AvilaandGitHub 9020239e1f Merge pull request #103 from HyunggyuJang/use-lock-file
Use npm ci rather than install to be consistent with lock file
2023-03-21 08:31:09 -04:00
Hyunggyu Jang 0a12b47760 Use npm ci rather than install to be consistent with lock file 2023-03-21 12:23:25 +09:00
Danny AvilaandGitHub 9358a4fdb5 Merge pull request #102 from danny-avila/bing-hotfix
bing hotfix, latest api, uses sydney
2023-03-20 17:16:45 -04:00
Daniel Avila 7d796f2c3e bing hotfix, latest api, uses sydney 2023-03-20 17:02:37 -04:00
Danny AvilaandGitHub 0a1651f6a1 Update README.md 2023-03-20 04:44:42 -04:00
Daniel Avila d13315c45b chore: comment out auth route 2023-03-20 02:58:41 -04:00
Daniel Avila 0b75d5d6fe chore: another hotfix patch on getConvoTitle 2023-03-20 02:30:08 -04:00
Wentao LyuandDaniel Avila 39819b744c doc: add magic of generate auth code 2023-03-20 02:22:04 -04:00
Daniel Avila 28c8f066d9 chore: add latest api as dep with alias 2023-03-20 02:18:03 -04:00
Daniel Avila c85602b93b chore: Replace hard coded message ID with unique one 2023-03-20 01:51:07 -04:00
Daniel Avila 08c91871c7 chore: hotfix: browser will not leave empty convo 2023-03-20 01:42:06 -04:00
Daniel Avila 80ca3bc375 chore: hotfix for browser client 2023-03-20 01:35:02 -04:00
HyunggyuJangandDaniel Avila 0405206438 Remove crypto from client side
Unless, if we tries to access the client side page from http protocol, crypto.randomUUID() isn't available.
2023-03-20 00:58:17 -04:00
Daniel Avila 0af8f6a699 chore: fix broken browser client 2023-03-20 00:51:56 -04:00
Daniel Avila b0936fa322 chore: unplug meilisearch, add leading option to throttle 2023-03-20 00:48:16 -04:00
Daniel Avila 4cd0ff2682 fix: throttle scroll to bottom 2023-03-19 11:45:03 -04:00
Daniel Avila 4ce60537ca search result styling changes 2023-03-19 11:25:12 -04:00
Daniel Avila 0b47218cd5 markdown library change 2023-03-19 01:14:19 -04:00
Daniel Avila d56aa2edef loads up to 20 messages, debugging markdown issue 2023-03-18 23:18:36 -04:00
Daniel Avila 4e6168d8fa setup message population on search 2023-03-18 18:40:53 -04:00
Daniel Avila 4197a92609 feat: search working as expected 2023-03-18 17:49:24 -04:00
Daniel Avila da42d6272a add loading state for fetching 2023-03-18 15:59:59 -04:00
Daniel Avila b97594c000 fix: conflicting fetch with /api/convos 2023-03-18 14:28:10 -04:00
Daniel Avila 0f54ffd8b4 feat: search bar working, still in progress 2023-03-18 01:40:49 -04:00
Daniel Avila 610cba4a60 backend logic drafted, moving to frontend 2023-03-17 22:20:36 -04:00
Daniel Avila 4f5ee8b198 move db functions 2023-03-17 19:58:13 -04:00
Daniel Avila 586c162404 merge commit 2023-03-17 19:40:44 -04:00
Danny AvilaandGitHub 1308ef1394 Merge pull request #88 from wtlyu/feat-clean-after-regenerate-pr
Feat clean after regenerate pr
2023-03-17 15:31:32 -04:00
Danny Avila 1513c27f7d adjust: wording and placeholder text size 2023-03-17 14:48:21 -04:00
Wentao Lyu 0ff3bbb28f clean up 2023-03-18 02:15:24 +08:00
Wentao Lyu 7987c0100c fix: add crypto as deps. [need confirm] 2023-03-18 02:04:51 +08:00
Wentao Lyu e8611a1d07 fix: don't reset isSubmitting in useEffect of submission 2023-03-18 01:54:06 +08:00
Wentao Lyu ce78123369 fix: allow delete of last char while submitting 2023-03-18 01:24:35 +08:00
Wentao Lyu a90db1f1a4 fix: add model to customGpts
fix: filter models by model only
2023-03-18 01:12:45 +08:00
Wentao Lyu a213868b17 fix: set isSubmitting with messages together
style: some notification
2023-03-18 01:12:45 +08:00
Wentao Lyu 6b2a2bb858 feat: save cancelled flag in message 2023-03-18 01:11:44 +08:00
Wentao Lyu fea3afa740 fix: missing import 2023-03-18 01:10:20 +08:00
Wentao Lyu 7372b37fe6 style: hide footer in mobile mode
style: avoid ui overflow in mobile mode
2023-03-18 01:09:59 +08:00
Danny AvilaandGitHub e11ce141d7 Merge pull request #86 from danny-avila/fix-scroll
fix: fix weird scrolling behavior on last message
2023-03-17 12:38:15 -04:00
Danny Avila 46fbd3b66a fix: fix weird scrolling behavior on last message 2023-03-17 12:34:54 -04:00
Danny AvilaandGitHub ce3f03267a Merge pull request #80 from wtlyu/feat-regenerate-and-cancel
Feat regenerate and cancel
2023-03-17 10:40:45 -04:00
Danny AvilaandWentao Lyu 9a2392e4d5 fix: mobile styling after regen buttons 2023-03-17 22:20:23 +08:00
Danny AvilaandGitHub 1eab4d240d Merge pull request #85 from danny-avila/bing-refusal
Bing refusal
2023-03-17 09:14:40 -04:00
Danny Avila 5568a60174 handle bing message refusal 2023-03-17 09:11:45 -04:00
Danny Avila ea4180f22a remove detect code (not always accurate) 2023-03-17 09:11:31 -04:00
Daniel Avila 6d2f3361d0 feat: search in progress 2023-03-16 21:20:40 -04:00
Daniel Avila 9995a159aa feat: reorganize api files, add mongoMeili 2023-03-16 19:38:16 -04:00
Daniel Avila 854f1c3572 feat: search, refactoring messages model 2023-03-16 17:20:26 -04:00
Danny Avila dcc13daf67 testing: mongo meilisearch 2023-03-16 16:22:08 -04:00
Wentao Lyu 2310bab348 revert: dont overwrite the mobile input panel color 2023-03-17 03:54:15 +08:00
Wentao Lyu d64edfdc7d feat: add chrome title color 2023-03-17 03:50:34 +08:00
Wentao Lyu a8c53f1f0d mobile style, for input panel and regenerate buttons 2023-03-17 03:50:34 +08:00
Wentao Lyu ef9f1ee1cf feat: cancellable api request 2023-03-17 03:50:34 +08:00
Wentao Lyu 66ad54168a feat: regenerate for bingai 2023-03-17 03:50:34 +08:00
Wentao Lyu 0891566d1e feat: add regenerate to all response message as official 2023-03-17 03:49:59 +08:00
Wentao Lyu e3b0cb7db7 feat: show model at the top of conversation messages. 2023-03-17 03:48:45 +08:00
Danny Avila c27554ed2e fix: allow customs in model filter 2023-03-16 15:05:23 -04:00
Danny AvilaandGitHub 87f793f1c4 Merge pull request #79 from danny-avila/improve-titles
fix: add deterministic titling
2023-03-16 15:03:04 -04:00
Danny Avila 4078c5283b fix: add deterministic titling 2023-03-16 15:02:40 -04:00
Danny AvilaandGitHub 5cac7e48f0 Merge pull request #74 from wtlyu/feat-model-based-on-key
feat: show model based on configured Keys
2023-03-16 15:00:23 -04:00
Danny AvilaandGitHub 7a08c77850 Update README.md 2023-03-16 14:11:24 -04:00
Danny AvilaandGitHub 1fe9e29187 Merge branch 'master' into feat-model-based-on-key 2023-03-16 13:39:41 -04:00
Danny AvilaandGitHub ba8692dbe4 Merge pull request #59 from wtlyu/feat-usersys
Feat usersys
2023-03-16 13:35:03 -04:00
Danny AvilaandGitHub d30b406c4c Merge pull request #4 from wtlyu/revisions
Revisions
2023-03-16 13:34:36 -04:00
Danny Avila 915cda70ef fix: user:user redundancies 2023-03-16 13:32:04 -04:00
Danny Avila df19595c5b fix: icon and mobile textarea styling 2023-03-16 13:30:10 -04:00
Danny Avila 867b3073d4 fix: icon and mobile textarea styling 2023-03-16 13:29:13 -04:00
Danny AvilaandGitHub e4e28dbbe2 Update README.md 2023-03-16 10:57:44 -04:00
Danny AvilaandGitHub cdbc0e21e7 Update README.md 2023-03-16 10:57:18 -04:00
Wentao Lyu b6f7f95709 feat: set default model once model list read. 2023-03-16 21:12:33 +08:00
Wentao Lyu 131af50034 feat: show model based on configured Keys 2023-03-16 14:44:14 +08:00
Wentao Lyu 23c050b54e style: mobile style of landing 2023-03-16 14:07:48 +08:00
Wentao Lyu 7442294c41 fix: mobile sizing style of icon 2023-03-16 14:05:12 +08:00
Wentao Lyu a47dbe6262 feat: use same icon style in TextChat 2023-03-16 13:45:46 +08:00
Wentao Lyu aabb19656e feat: combine customgpt to user 2023-03-16 13:30:20 +08:00
Wentao Lyu b0284b6974 sync updates and merge with feat-resubmit 2023-03-16 13:20:54 +08:00
Wentao Lyu 62d88380e0 feat: add sample multi-user support
feat: update README
2023-03-16 13:20:34 +08:00
Daniel Avila 41f351786f fix: oversight, artifact in bing route 2023-03-15 22:36:16 -04:00
Danny AvilaandGitHub ffcfb69dee Merge pull request #72 from danny-avila/fix-mobile-switch
fix: mobile view for sibling switch
2023-03-15 19:06:41 -04:00
Daniel Avila c91ce36227 fix: mobile view for sibling switch 2023-03-15 19:05:17 -04:00
Danny AvilaandGitHub d06e58f043 Merge pull request #55 from wtlyu/feat-resubmit
Feature: multipath message & resubmit, conversation placeholder, separate title generation, and some bug fix.
2023-03-15 18:06:35 -04:00
Daniel Avila d052d221dc chore: switch focus to textarea when custom model change (still need to figure out reg model change) 2023-03-15 18:05:34 -04:00
Danny AvilaandGitHub ff45511011 Merge pull request #3 from wtlyu/final-adj
Final adjustments
2023-03-15 17:08:18 -04:00
Danny Avila 8c6340aed0 chore: refactor cursor blink, debugging 2023-03-15 16:38:01 -04:00
Danny Avila 84b104e65f chore: delegate response text parsing to one location 2023-03-15 15:44:48 -04:00
Danny Avila a0c94715ce chore: refactor titleConvo 2023-03-15 15:21:04 -04:00
Danny Avila a8aad30fc8 chore: memoized Messages component, will require custom equality check 2023-03-15 14:36:17 -04:00
Danny Avila 2fd50c99b8 fix: debounce title request and handle error with default title 2023-03-15 12:47:30 -04:00
Danny Avila 96ca783517 chore: re-organize message modules, fix icon size, convo reset properly rebuilds Tree 2023-03-15 10:42:45 -04:00
Wentao Lyu 45ca0a8713 fix: gptCustom icon should show as same in model and message 2023-03-15 14:42:39 +08:00
Wentao Lyu 5d0b849930 feat: show icon within model select menu
fix: use icon for gptCustom
2023-03-15 14:21:08 +08:00
Wentao Lyu 54aa9debb4 fix: don't reset new convo if model not change
fix: change model will clear all messages.
2023-03-15 13:38:01 +08:00
Daniel Avila 4e91437049 fix: convo resets, sets new Convo 2023-03-14 21:32:25 -04:00
Daniel Avila 918f2fecb6 fix: convo resets on model change 2023-03-14 21:25:02 -04:00
Daniel Avila 796d8031e8 fix: ensure custom params are not passed to non custom models 2023-03-14 20:21:41 -04:00
Daniel Avila 6e32f71565 fix: adjust custom client for new progress CB 2023-03-14 20:15:06 -04:00
Daniel Avila 6192c2964e fix: validation to avoid saving customGpt params to non-custom models 2023-03-14 20:14:38 -04:00
Daniel Avila 626a8fbd8e fix: if db is empty, will never show new convos until refresh 2023-03-14 18:53:46 -04:00
Danny AvilaandGitHub a8344ec5bf Merge pull request #2 from wtlyu/refactors
Refactors
2023-03-14 18:16:44 -04:00
Danny AvilaandGitHub c230fe41f4 Merge branch 'feat-resubmit' into refactors 2023-03-14 16:09:25 -04:00
Danny Avila d0ef0f84c8 chore: delegate text handling to one place, html sanitization in progress 2023-03-14 16:05:46 -04:00
Wentao Lyu 8289558d94 feat: pagination in nav 2023-03-15 04:05:14 +08:00
Danny Avila 2e20b28c4d chore: refactor progressCB to one place, fix sydney, and sanitize html 2023-03-14 15:42:59 -04:00
Danny Avila 9a17e94f8f fix: refactor migration and sort old convos correctly 2023-03-14 14:51:26 -04:00
Wentao Lyu 71fc86b9a6 fix: buildTree should store parent-not-exist message as root. rather than dropping them. 2023-03-15 02:43:21 +08:00
Wentao Lyu 8882432210 fix: hide the edit button when bingai 2023-03-15 02:33:08 +08:00
Wentao Lyu 644f3f716f feat: re-orginazed three ask api. To provide ability to reproduce message.
feat: bing and sydney come to work again, [need more test]
2023-03-15 02:15:46 +08:00
Wentao Lyu 8b00805d24 fix: dont send gen_title twice 2023-03-15 02:15:46 +08:00
Wentao Lyu d73375958b feat: return error as a error message, not only text 2023-03-15 02:15:46 +08:00
Wentao Lyu 7168498543 fix: jailbreakConversationId=false response will be saved jailbreakConversationId='false' in database. 2023-03-15 02:15:46 +08:00
Wentao Lyu 27515cb00a fix: generate title by backend 2023-03-15 02:15:46 +08:00
Daniel AvilaandWentao Lyu 3e7ce67609 fix: migrate old schema to new 2023-03-15 02:15:46 +08:00
Daniel AvilaandWentao Lyu 4fd05e15b4 fix: migrate old schema to new 2023-03-15 02:15:46 +08:00
Wentao Lyu 0fa19bb6ad feat: save error message into database. 2023-03-15 02:15:46 +08:00
Wentao Lyu d9e5464b3b fix: cleanup debug msg 2023-03-15 02:15:46 +08:00
Wentao Lyu 953c5fc970 fix: w<1024px, will overflow. 2023-03-15 02:15:46 +08:00
Wentao Lyu 2afbc5883f fix: use onCompositionStart and onCompositionEnd to aviod enter submit when using input method. 2023-03-15 02:15:46 +08:00
Wentao Lyu 953f846958 fix: loading and send button, mobile style
feat: sibling switch, mobile style
fix: only the real submitting message will blink
feat: drop the text version username, use a similar square. (or it will mass up the sibling switch)
2023-03-15 02:15:46 +08:00
Wentao Lyu a4d5f6a3f2 feat: fully multipath and resubmit 2023-03-15 02:15:46 +08:00
Wentao Lyu 4a39965b22 fix: add proxy to titleConvo 2023-03-15 02:15:46 +08:00
Wentao Lyu 90dc171b34 test: generate of title shouldn't be mislead by answer 2023-03-15 02:15:46 +08:00
Wentao Lyu 0e98cb4206 fix: in mobile view, resubmit edit button should always visible 2023-03-15 02:15:46 +08:00
Wentao Lyu 9f8e9cb091 feat: gen title by sperate api call
feat:

fix: rename of convo should based on real request.
2023-03-15 02:15:46 +08:00
Wentao Lyu 8773878be2 feat: create conversation at the beginning then return the userMessage 2023-03-15 02:15:46 +08:00
Wentao Lyu 5a409ccfa6 fix: don't resubmit html label
fix: hide the resubmit editor border
2023-03-15 02:15:46 +08:00
Wentao Lyu 0ed8a40a41 feat: merge all message.id into message.messageId
feat: the first message will have a parentMessageId as 00000000-0000-0000-0000-000000000000 (in order not to create new convo when resubmit)
feat: ask will return the userMessage as well, to send back the messageId

TODO: comment out the title generation.
TODO: bing version need to be test

fix: never use the same messageId
fix: never delete exist messages
fix: connect response.parentMessageId to the userMessage.messageId
fix: set default convo title as new Chat
2023-03-15 02:15:46 +08:00
Wentao Lyu be71140dd4 fix: new message should append to the exist one 2023-03-15 02:15:46 +08:00
Wentao Lyu 6d51ec3e37 feat: auto hide edit button when edit is enabled. 2023-03-15 02:15:46 +08:00
Wentao Lyu bdfc895800 feat: support resubmit.
TODO: basic implementation. should add multi-path record in future.

feat: add deleteMessahesSince
feat: saveMessage will do createOrSave
feat: reorginazed submission
2023-03-15 02:15:46 +08:00
Wentao Lyu b9975ac283 fix: missing setSubmission 2023-03-15 02:15:46 +08:00
Wentao Lyu dd1f74da72 replace all created to timestamps: true in db 2023-03-15 02:15:46 +08:00
Danny AvilaandGitHub 5d65427a6e Merge pull request #56 from HyunggyuJang/build/docker-from-rootdir
Build docker files from root dir
2023-03-13 13:17:23 -04:00
Danny AvilaandGitHub 6a16d31f26 Merge pull request #60 from wtlyu/feat-autofocus
feat: auto focus
2023-03-13 13:15:43 -04:00
Wentao Lyu 8affbfdb4a feat: auto focus 2023-03-14 01:12:11 +08:00
Hyunggyu Jang 70d59c5b3c Build docker files from root dir 2023-03-13 22:49:40 +09:00
Danny AvilaandGitHub 73979ee67f Merge pull request #54 from danny-avila/minor-fixes
fix: model menu key issue
2023-03-12 16:46:25 -04:00
Daniel Avila 8e513d83a5 fix: model menu key issue 2023-03-12 16:45:44 -04:00
Danny AvilaandGitHub 9bc85ea83d Merge pull request #47 from wtlyu/feat-title-with-language
fix: give a stronger prompt to generate tile using the input language
2023-03-12 15:10:45 -04:00
Wentao Lyu 943eb5c74d fix: give a stronger prompt to generate tile using the input language 2023-03-13 01:51:32 +08:00
Danny AvilaandGitHub bac37cfe36 Update README.md 2023-03-11 23:29:27 -05:00
Danny AvilaandGitHub b2334054da Update README.md 2023-03-11 23:28:56 -05:00
Danny AvilaandGitHub a705e907ff Merge pull request #45 from danny-avila/submit-state
fixes: allow simultaneous convos & minor improvements
2023-03-11 23:28:33 -05:00
Daniel Avila c8bb6b13bf update: readme.md 2 2023-03-11 23:24:53 -05:00
Daniel Avila 50a17bc1b8 update: readme.md 2023-03-11 23:22:51 -05:00
Daniel Avila 3038015a5b fix: removes newline from start of stream response if present 2023-03-11 23:11:55 -05:00
Daniel Avila 20da895ee7 update bypass proxy url for browser client 2023-03-11 23:08:04 -05:00
Daniel Avila 8762765dd0 fix: retain scroll view on delete convo 2023-03-11 22:59:32 -05:00
Daniel Avila f1aabfa543 fix: reset submission when convo clears 2023-03-11 22:35:39 -05:00
Daniel Avila 23de688bf3 fix: stops stream upon conversation 2023-03-11 21:42:08 -05:00
Daniel Avila 79f050bac7 feat: loading state for messages 2023-03-11 18:39:46 -05:00
Danny AvilaandGitHub afae13eec6 Merge pull request #43 from danny-avila/fix-custom-name
fix: format sender text change and mobile styling
2023-03-11 17:38:17 -05:00
Daniel Avila 4fa599e97a fix: format sender text change and mobile styling 2023-03-11 17:36:10 -05:00
Danny AvilaandGitHub 3b1d6aaa79 Merge pull request #42 from danny-avila/title-lang
feat: title the chat in user's language
2023-03-11 16:28:36 -05:00
Daniel Avila 339230cd74 feat: title the chat in user's language 2023-03-11 16:26:41 -05:00
Daniel Avila 95bad60f7d fix: detectCode bug 2023-03-11 14:46:21 -05:00
Danny AvilaandGitHub 950b8f3f1e Merge pull request #34 from wtlyu/feat-mobile
feat: basic support mobile mode.
2023-03-11 12:59:09 -05:00
Danny AvilaandGitHub 0e2e5b8393 Update README.md 2023-03-11 12:32:59 -05:00
Danny AvilaandGitHub d2cb9957fe Merge pull request #35 from danny-avila/rm-short-filter
fix: rm short filter
2023-03-11 12:10:56 -05:00
Daniel Avila 78b4004ead fix: rm short filter 2023-03-11 12:10:00 -05:00
Danny AvilaandGitHub 8624062488 Merge pull request #26 from wtlyu/master
support config host name and proxy address. and fix a docker bug
2023-03-11 12:05:16 -05:00
Wentao Lyu 070fee2ece feat: basic support in mobile mode. including:
navbar show and hide,
similar fade animation,
auto close when select new convo,
mobile title will change with convo,
new chat button.
2023-03-12 00:32:03 +08:00
Danny AvilaandGitHub b1569450fd Merge pull request #32 from danny-avila/fix-hjs
Fix hjs
2023-03-11 10:32:38 -05:00
Daniel Avila 09659be002 fix: unsupported lang handler in client 2023-03-11 10:28:46 -05:00
Wentao Lyu 5e4fa09dcb update readme, add proxy and host information 2023-03-11 16:14:24 +08:00
Wentao Lyu 16c9589058 fix: use quotes outside of key=value in docker-compose.yml
fix: set HOST=0.0.0.0 by default in api's dockerfile
2023-03-11 16:14:11 +08:00
Wentao Lyu 8604030404 Add more information in env.example.
Give current host address when HOST=0.0.0.0
2023-03-11 15:03:18 +08:00
Wentao Lyu 97668217b9 fix: add proxy to bingai too 2023-03-11 15:02:26 +08:00
Wentao Lyu c5c865a25f fix: frontend api request shouldn't hard code the domain and port. 2023-03-11 14:13:39 +08:00
Wentao Lyu 06c99154ac feat: support config host name and proxy address 2023-03-11 14:12:51 +08:00
Daniel Avila 9d71b58345 fix: handles unsupported hjs languages in client response 2023-03-10 22:21:15 -05:00
Danny AvilaandGitHub fb9f77ae5e Merge pull request #23 from danny-avila/tab-links
Add back Tab links
2023-03-10 13:18:54 -05:00
Danny Avila fc2b9bf7f2 feat: tab link component, edit wrapper 2023-03-10 13:12:24 -05:00
Danny Avila cdbe00ec6f feat: tab link component 2023-03-10 13:11:17 -05:00
Danny Avila 72ff47e204 Rolled back to v0.0.2 2023-03-10 12:55:45 -05:00
Danny AvilaandGitHub 57d3025717 Merge pull request #20 from danny-avila/override-links
feat: links open in new tab
2023-03-10 09:45:52 -05:00
Danny Avila 6c02558f1b feat: links open in new tab 2023-03-10 09:44:40 -05:00
Danny AvilaandGitHub 0c62863e52 Update README.md 2023-03-10 08:38:45 -05:00
Danny Avila d825721dad fix: update readme and compose file 2023-03-10 08:37:15 -05:00
Danny AvilaandGitHub 0b3b6f91fc Merge pull request #19 from wtlyu/master
support config host name and proxy address.
2023-03-10 08:21:22 -05:00
Wentao Lyu 3a9b532248 fix: frontend api request shouldn't hard code the domain and port. 2023-03-10 21:12:16 +08:00
Wentao Lyu 67156f4a7a feat: support config host name and proxy address 2023-03-10 21:12:16 +08:00
Daniel Avila 4e616cd2ed fix: error handling titles, remove openAI req 2023-03-10 07:43:43 -05:00
Danny AvilaandGitHub 12cf3405e4 Merge pull request #16 from danny-avila/updateAI
feat: add sydney (jailbroken bing) and more bing styling with citations
2023-03-09 20:34:28 -05:00
Daniel Avila 4383894ad3 finish update 2023-03-09 20:31:56 -05:00
Daniel Avila d0ecaabfd8 feat: cites text with links 2023-03-09 20:29:44 -05:00
Daniel Avila a286f027c8 model menu won't close on model deletion 2023-03-09 19:02:59 -05:00
Daniel Avila 0ba92c4c03 add new reducer to ensure conversations are renewed on model change 2023-03-09 18:57:37 -05:00
Daniel Avila 30936573ac feat: includes sources 2023-03-09 18:42:36 -05:00
Daniel Avila a451574760 feat: cites text with links 2023-03-09 18:07:36 -05:00
Danny Avila 5e57deab5f bing styling in progress 2023-03-09 16:09:53 -05:00
Daniel Avila eae82edb83 merge readme update 2023-03-08 22:33:25 -05:00
Daniel Avila 1e0cbbf1cc merge readme update 2023-03-08 22:32:51 -05:00
Daniel Avila 3986b4ec9e Merge branch 'master' of https://github.com/danny-avila/chatgpt-clone into updateAI 2023-03-08 22:31:40 -05:00
Daniel Avila 67054e7504 working sydney, need to test the other models 2023-03-08 22:30:29 -05:00
Daniel Avila 2c1ae68dc4 adding sydney in progress. only uses jailbreakConvoId and parentMsgId 2023-03-08 21:06:58 -05:00
Daniel Avila 69b3edc52c feat: sydney is functional 2023-03-08 19:47:23 -05:00
Danny AvilaandGitHub 732c75d145 Update README.md 2023-03-08 10:26:12 -05:00
Danny AvilaandGitHub 0f54251459 Merge pull request #12 from HyunggyuJang/fix/webpack-artifacts
webpack artifacts should exist for nginx image
2023-03-07 21:07:13 -05:00
Hyunggyu Jang 72ef76b8a0 webpack artifacts should exist for nginx image 2023-03-08 10:38:22 +09:00
Danny AvilaandGitHub 81abbdb335 Merge pull request #11 from HyunggyuJang/fix/docker-name
DockerFile is invalid name
2023-03-07 20:11:44 -05:00
Hyunggyu Jang 67ca79dd3f DockerFile is invalid name 2023-03-08 09:51:04 +09:00
Danny AvilaandGitHub 5af03bb3d8 Update README.md 2023-03-07 14:54:44 -05:00
Danny AvilaandGitHub e5d25f45e3 Merge pull request #7 from danny-avila/easy-config-setup
dockerize app, improve ui/ux, add setup instructions
2023-03-07 14:53:57 -05:00
Danny Avila ad967beaf8 update readme 2023-03-07 14:51:41 -05:00
Danny Avila a8eaf2452a update readme, setup files, and clear convo bug 2023-03-07 14:43:55 -05:00
Danny Avila 5ad32e211f update docker-compose 2023-03-07 14:23:56 -05:00
Danny Avila da61a2841f fix: customGpt persistence blindspot, also remove free gpt 2023-03-07 14:22:33 -05:00
Danny Avila da55ac3774 update readme 2023-03-07 13:58:57 -05:00
Danny Avila f2b092545f fix: convos correctly delete 2023-03-07 13:53:23 -05:00
Danny Avila 17c01aa252 fix: ignoreDir pointing correctly 2023-03-07 13:49:15 -05:00
Danny Avila 6be037323e chore: model menu functions behave as expected 2023-03-07 13:19:56 -05:00
Daniel Avila 191118b90b add buttons to custom model menu items 2023-03-06 21:43:49 -05:00
Daniel Avila 6d757ff91f improve custom model selection ux 2023-03-06 21:12:36 -05:00
Daniel Avila d93282640c modify starting files and readme 2023-03-06 18:17:43 -05:00
Daniel Avila b9e9203a75 Merge branch 'easy-config-setup' of https://github.com/danny-avila/chatgpt-clone into easy-config-setup 2023-03-06 16:59:03 -05:00
Danny Avila ee8eba9ad5 updating readme 2023-03-06 16:30:16 -05:00
Danny Avila 102327313e modify docker setup 2023-03-06 15:57:01 -05:00
Danny Avila 08f59819cf modify docker setup 2023-03-06 15:56:25 -05:00
Danny Avila d9930dd2bc edit docker setup 2023-03-06 14:38:34 -05:00
Danny Avila 453301db0e chore: edit readme and docker-compose 2023-03-06 14:30:58 -05:00
Danny Avila 52e529dcbe complete docker setup 2023-03-06 14:04:06 -05:00
Danny Avila ff7b016190 initial docker detup 2023-03-06 12:49:22 -05:00
Danny Avila f5e079742a chore: reorganize client files for docker 2023-03-06 10:47:06 -05:00
Danny Avila affbaaf1a5 chore: server setup complete 2023-03-06 10:28:37 -05:00
Danny Avila fca546af63 reorganize dirs for dockerize 2023-03-06 10:15:07 -05:00
Danny AvilaandGitHub 062cd1d838 Merge pull request #6 from danny-avila/easy-config-setup
chore: misc. fixes/improvements before containerizing
2023-03-06 09:27:14 -05:00
Danny Avila 6ae154cc42 optimize convo paginations 2023-03-06 08:58:52 -05:00
Danny Avila c617b15bcd disable prompts until complete 2023-03-06 08:50:36 -05:00
Danny Avila b750bd2970 update roadmap 2023-03-06 08:41:09 -05:00
Daniel Avila 8d60cbdc7a feat: dynamic custom model saving/updating in dialog 2023-03-05 17:02:00 -05:00
Daniel Avila 15986c078b update readme with 2nd use case img 2023-03-05 16:23:35 -05:00
Daniel Avila 15f3972b9f remove models caching 2023-03-05 15:30:40 -05:00
Danny AvilaandGitHub 3224dca3b8 Merge pull request #5 from danny-avila/convo-pagination
feat: convo pagination in increments of 12
2023-03-05 14:42:43 -05:00
Daniel Avila a98367d27f convo fetching in increments of 12 2023-03-05 14:41:50 -05:00
Danny AvilaandGitHub 80319c7fcc Merge pull request #4 from danny-avila/dark-mode-match
feat: match more official stylings
2023-03-05 13:32:59 -05:00
Daniel Avila dae7f8d822 match more official stylings 2023-03-05 13:31:12 -05:00
Danny AvilaandGitHub c8d8484d86 Merge pull request #2 from danny-avila/customChatGPT
feature: customize chat gpt
2023-03-04 21:47:48 -05:00
Daniel Avila f40f80f030 update readme 2023-03-04 21:46:58 -05:00
Daniel Avila fc899da944 convo change also changes model selection 2023-03-04 21:10:45 -05:00
Daniel Avila d64f914552 complete customGpt 2023-03-04 20:48:59 -05:00
Daniel Avila b4b0c123ba prompt prefix and label state changes when customGpt selected 2023-03-04 18:00:12 -05:00
Daniel Avila 9c3a78f96b customGpts persist through localStorage 2023-03-04 17:39:06 -05:00
Daniel Avila 62bb6ea8f8 refactor model menu items into component 2023-03-03 21:33:09 -05:00
Daniel Avila 0bec306e59 remove punctuation from titles 2023-03-03 21:31:37 -05:00
Daniel Avila 42c74f254c reduce stylesheet clutter 2023-03-03 21:31:05 -05:00
Danny Avila 9a56130750 save custom settings in progress 2023-03-03 16:33:02 -05:00
Danny Avila 214228542a complete customChatGpt model selection 2023-03-03 15:52:06 -05:00
Danny AvilaandGitHub 0f9cfd0395 Merge pull request #1 from danny-avila/copyCode
complete copyCode for code blocks
2023-03-03 09:46:49 -05:00
Danny Avila 92648554ad complete copyCode for code blocks 2023-03-03 09:45:20 -05:00
Danny Avila ac2229fbb6 add more detail to titleConvo prompt 2023-03-03 09:07:29 -05:00
Danny Avila ec7aaf01a4 add hoverbuttons 2023-03-03 08:51:33 -05:00
Danny Avila 50ff96cbc5 copy code in progress 2023-03-02 16:31:00 -05:00
Danny Avila 0d8755a03a update readme 2023-03-02 16:08:26 -05:00
Danny Avila d235bb7b1b complete language detection for code blocks 2023-03-02 16:07:36 -05:00
Danny Avila c4d0787b49 chatgpt now titles conversations 2023-03-02 13:52:41 -05:00
Daniel Avila 897c384ac9 fix code parsing bug 2023-03-01 21:59:13 -05:00
Daniel Avila cbcc1a1bb4 readme update 2023-03-01 21:41:24 -05:00
Daniel Avila 1f270d349a add official api, remove davinci, organize root dir 2023-03-01 21:39:57 -05:00
Danny AvilaandGitHub 90ac43c8c5 Create LICENSE.md 2023-02-28 17:21:29 -05:00
Daniel Avila 1bba86f2e2 minor changes 2023-02-26 14:10:44 -05:00
Daniel Avila 8e5148b390 fix embed wrapper not invoking immediately 2023-02-25 10:33:26 -05:00
Daniel Avila 0ee31f0443 fix codeWrapper issue, will produce funky text with ` commenting 2023-02-25 10:16:21 -05:00
Daniel Avila ed699d2c06 wrap code in progress, revert old message handling 2023-02-25 09:06:26 -05:00
Daniel Avila 0f6fcd5928 organize modules 2023-02-25 09:04:32 -05:00
Daniel Avila 78c9f3dc74 trying different markdown library for proper list styling 2023-02-24 23:16:19 -05:00
Danny Avila 34ebd97c9d indicate initial gpt response 2023-02-24 16:31:59 -05:00
Danny Avila 61d898c387 fix bing chat not persisting after 1st invocation 2023-02-24 16:20:23 -05:00
Daniel Avila fd01fd3334 adds markdown support, fix code wrapping 2023-02-24 11:24:09 -05:00
Daniel Avila c58a9bbe93 finish highlight.js styling (for chatgpt) 2023-02-23 23:56:55 -05:00
Danny Avila e95e22de15 code highlighting in progress 2023-02-23 16:32:08 -05:00
Daniel Avila 187f7b5b03 handles in-line code in model response text (not multi-line yet) 2023-02-22 22:42:22 -05:00
Daniel Avila 1a6cddb8bb persist models in localStorage, minor changes 2023-02-22 21:30:48 -05:00
Daniel Avila e2a3ecbf88 update readme 2023-02-21 21:41:30 -05:00
Daniel Avila a516b38e27 adds linting, fix linter errors, abort automatic scrolling 2023-02-21 21:31:36 -05:00
Daniel Avila 16932b37c0 fix bingai bugs and add chatgptbrowser client 2023-02-21 21:30:56 -05:00
Daniel Avila 168d5e8075 fix linter errors 2023-02-21 19:40:28 -05:00
Daniel Avila 7b7548c905 persist bing ai conversation 2023-02-20 21:52:08 -05:00
Daniel Avila 81e52fed73 properly records bing conversations 2023-02-20 21:16:40 -05:00
Daniel Avila b4e22936ba update schema for bing options 2023-02-19 21:06:21 -05:00
Danny Avila ac2e897709 adds og bing icon 2023-02-16 14:06:18 -05:00
Danny Avila 5c7000c273 adds bing ai functionality, still need to code out message/convo storing since it returns a lot of useful parameters 2023-02-15 12:29:56 -05:00
Daniel Avila e0a26f52cd update readme 2023-02-15 00:06:10 -05:00
Daniel Avila f18317f7b7 useDocumentTitle changes title on first title change, scrollBar renders when expected 2023-02-14 21:23:14 -05:00
Daniel Avila 7347eda219 update readme 2023-02-14 19:46:24 -05:00
Daniel Avila e1b615835a new demo gif 2023-02-14 19:38:28 -05:00
Daniel Avila eaa0242e10 new demo gif 2023-02-14 19:11:47 -05:00
Daniel Avila 481927c5a3 adds demo gif 2023-02-14 19:03:48 -05:00
Daniel Avila 0833d795bd readme update and footer link update 2023-02-14 18:40:00 -05:00
Daniel Avila 7d7a4dd905 proxy server is down 2023-02-14 18:34:46 -05:00
Daniel Avila e520973203 draft readme 2023-02-14 18:31:29 -05:00
Danny Avila 9c225a9bb3 drafting readme 2023-02-14 16:33:22 -05:00
Danny Avila 742073fb89 refactor convos fetching to nav components, results in faster loading 2023-02-14 16:15:45 -05:00
Daniel Avila e4d1ff2523 rearrange convo fetching for performance in progress 2023-02-13 23:14:35 -05:00
Daniel Avila c00a2c902b front and backend logic for model switching 2023-02-13 21:15:28 -05:00
Daniel Avila a5afd5c48f animates scroll to bottom and debounces bottom 2023-02-13 20:13:59 -05:00
Daniel Avila 779f142058 fix static flag bug 2023-02-13 18:02:29 -05:00
Danny Avila 92b2109dc3 adds scrolltobottom button, still handling initial state when scrollbar is present 2023-02-13 16:31:18 -05:00
Danny Avila 583fd56ca5 adds shadcn-ui components, and model button to switch chat models 2023-02-13 15:58:35 -05:00
Danny Avila acaef39d12 refactors server route for brevity, state working, styling matching to chatgpt 2023-02-13 13:32:54 -05:00
Danny Avila dcf4f2a524 debugging convo state, useSWR now within convos component 2023-02-13 10:20:21 -05:00
Daniel Avila 0e5c7c7721 fixing convo state in progress 2023-02-12 22:57:59 -05:00
Daniel Avila 500b6c9fa8 adds functionality for waylaidwanderer chatgpt client 2023-02-12 22:24:36 -05:00
Daniel Avila 5b8c4452cc remove unused packages and minor changes 2023-02-12 16:48:15 -05:00
Daniel Avila ed44daf8b3 refactor express to use routes 2023-02-12 16:38:33 -05:00
Daniel Avila cd71bb0727 remove nodemon from server script 2023-02-11 17:35:07 -05:00
Daniel Avila 48c591a8b9 scrollbar styling 2023-02-11 13:48:48 -05:00
Daniel Avila 8eeac36b35 update press release 2023-02-11 13:05:48 -05:00
Daniel Avila 4cecbcf642 update press release 2023-02-11 13:02:30 -05:00
Daniel Avila 8432cbd293 update press release 2023-02-11 13:01:58 -05:00
Daniel Avila 256b60435f update press release, minor change in textchat component, remove unused package 2023-02-11 13:01:03 -05:00
Daniel Avila d9d5f4a509 delegate footer to own component 2023-02-11 12:54:02 -05:00
Daniel Avila e8c514fff9 delete updates state title 2023-02-11 12:49:07 -05:00
Daniel Avila ef4ec39a76 update press release notes 2023-02-11 11:54:17 -05:00
Daniel Avila 4304597cb4 small detail for renaming convos 2023-02-11 11:54:03 -05:00
Daniel Avila 7dd4e78bbf tests scrolling to top, show/hide templates 2023-02-11 11:37:20 -05:00
Daniel Avila 5af5a97d8f complete renaming functions, abstracts more svg, sets title to current convo title, adds a try again feature to errors 2023-02-11 10:22:15 -05:00
Daniel Avila 592b7629aa abstracts svgs, fiddling with prompt templates idea 2023-02-10 21:00:02 -05:00
Danny Avila cab4b70b7e took a screenshot of use case 2023-02-09 16:21:36 -05:00
Daniel Avila c36f752551 fix scrolling bug 2023-02-08 22:58:24 -05:00
Danny Avila 217bdb4865 client error handling, regen response without saving messages 2023-02-08 15:26:42 -05:00
Danny Avila 87685c3791 conversation conditional rendering 2023-02-08 11:05:54 -05:00
Danny Avila 0c6dc8665b dark mode alternates to light mode on dark theme, error state in progress 2023-02-08 10:49:38 -05:00
Danny Avila 5cd50e7826 handle textarea text state with redux and setText on example click 2023-02-08 09:59:01 -05:00
Danny Avila 581ee0e2ca adds favicon to head 2023-02-08 09:46:26 -05:00
Danny Avila d07047722e adds favicon to head 2023-02-08 09:44:10 -05:00
Danny Avila b5042a738e adds dark mode and logs errors in message response 2023-02-08 09:15:47 -05:00
Danny Avila 58498ed951 clears conversations and no new line on enter 2023-02-08 08:27:23 -05:00
Daniel Avila 51d93da3f8 adds server and client error handling, clear convos in progress 2023-02-08 00:02:29 -05:00
Daniel Avila 511ac948b4 clear all convos in progress, async code refactored in debugging warning 2023-02-07 19:07:48 -05:00
Daniel Avila 053368646d useeffect triggers react warning 'expected static flag was missing' but workarounds don't look as nice 2023-02-07 19:07:12 -05:00
Danny Avila 6842ac880c optimistic ui for message sending and submit state 2023-02-07 16:22:35 -05:00
Danny Avila 9d41ed4615 reorganize components and add root import plugin 2023-02-07 10:26:19 -05:00
Danny Avila faf8800e67 remove sourcemap error 2023-02-07 09:59:45 -05:00
Danny Avila 36ac055ae5 Complete rewire state to redux 2023-02-07 09:41:54 -05:00
Daniel Avila 7978ddd871 adding redux in progress 2023-02-07 00:05:00 -05:00
Daniel Avila 85efaa4173 adding redux to reduce prop drilling and allow for advanced conversation state 2023-02-06 21:18:28 -05:00
Daniel Avila 6e3f63ee46 adding clear methods for convos 2023-02-06 21:17:46 -05:00
Daniel Avila c7c50dbbab convo successfully changes on convo click 2023-02-06 18:25:11 -05:00
Danny Avila d35ed31b20 convo selection handling in progress 2023-02-06 16:28:50 -05:00
Danny Avila c794287ced load convos on message send & other conditional rendering 2023-02-06 16:00:59 -05:00
Danny Avila 2869638cc0 get convos endpoint 2023-02-06 15:17:54 -05:00
Danny Avila c821d1eee5 made model method one line shorter 2023-02-06 14:58:43 -05:00
Danny Avila 4e6b6be15c titles conversations based on first two messages 2023-02-06 14:57:47 -05:00
Danny Avila 511c8c9599 saves all conversations with messages 2023-02-06 14:05:02 -05:00
Danny Avila 232a823b9b store all messages to mongoDB 2023-02-06 13:28:21 -05:00
Danny Avila f2003da779 layout mimics actual chatgpt ui 2023-02-06 13:27:28 -05:00
Daniel Avila 254f9d7e26 adds db connectivity and saving, need to process user messages still 2023-02-05 23:05:07 -05:00
Daniel Avila f889f23792 chatgpt is taking shape, convo persists, layout mimics original 2023-02-05 19:41:24 -05:00
Daniel Avila 3a199757ae sse events on text submit 2023-02-05 15:29:35 -05:00
Daniel Avila 27f7276fc8 working SSE stream 2023-02-05 15:26:59 -05:00
Daniel Avila 81866f197a webpack config for polyfills 2023-02-04 20:49:58 -05:00
Daniel Avila 502bdc5dd5 building potential Server side events 2023-02-04 20:49:42 -05:00
Daniel Avila 641dcfad73 chat methods defined, also test file for library testing 2023-02-04 20:49:01 -05:00
Daniel Avila 7c4e7ab07a abstracts textarea 2023-02-04 20:48:33 -05:00
Daniel Avila 7b9f3ee9c2 update env variables and build 2023-02-04 20:09:58 -05:00
Daniel Avila 39781f5318 testing chatgpt logic 2023-02-04 20:09:02 -05:00
Daniel Avila 521a8eb704 templating model 2023-02-04 20:08:46 -05:00
Daniel Avila 92860a1bb4 adds express server 2023-02-04 20:08:28 -05:00
Daniel Avila 037a12a282 logs textarea input 2023-02-04 20:07:37 -05:00
Daniel Avila 5dc2d5efba updates to react 18 method createRoot 2023-02-04 20:07:08 -05:00
Daniel Avila 21a6a0587c adds basic layout 2023-02-04 19:19:53 -05:00
Daniel Avila 0fb3a1cab3 adds tailwindcss 2023-02-04 18:17:47 -05:00
Daniel Avila 74232d7671 initial commit, project is ready to start 2023-02-04 17:17:17 -05:00
hackreactor-bot 466dd01327 (init) 2022-10-20 11:45:11 +00:00
1407 changed files with 47824 additions and 97700 deletions
-25
View File
@@ -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"
]
}
-46
View File
@@ -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:
+2
View File
@@ -0,0 +1,2 @@
**/node_modules
**/.env
-38
View File
@@ -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
View File
@@ -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']
-75
View File
@@ -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
-46
View File
@@ -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
-14
View File
@@ -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 &
-19
View File
@@ -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
}
-7
View File
@@ -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}"
-11
View File
@@ -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
-64
View File
@@ -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 }}"
-100
View File
@@ -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
-38
View File
@@ -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
View File
@@ -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
-52
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
.devcontainer/docker-compose.yml
frontend/
-88
View File
@@ -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"
]
}
]
}
-18
View File
@@ -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
View File
@@ -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;"]
-661
View File
@@ -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
View File
@@ -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.
+87
View File
@@ -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
View File
@@ -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
+328 -204
View File
@@ -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
![Alt](https://repobeats.axiom.co/api/embed/8e66d9bc24dd93b7a61c804484af54233225d857.svg "Repobeats analytics image")
## 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**
![use case example](./images/use_case3.png "Make a Custom GPT")
- **API is not as limited as ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat))**
![use case example](./images/use_case2.png "chat.openai.com is getting more limited by the day!")
- **ChatGPT Free is down.**
![use case example](./images/use_case.png "GPT is down! Plus is too expensive!")
</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.
+70
View File
@@ -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
+39
View File
@@ -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'
}
};
+22
View File
@@ -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"
}
+62
View File
@@ -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 };
+45
View File
@@ -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 };
+34
View File
@@ -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 };
+35
View File
@@ -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;
+40
View File
@@ -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 };
+19
View File
@@ -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,
};
+75
View File
@@ -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;
+44
View File
@@ -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;
+70
View File
@@ -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;
+63
View File
@@ -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;
+31
View File
@@ -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;
+13
View File
@@ -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;
+29
View File
@@ -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;
+15
View File
@@ -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
};
+59
View File
@@ -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 };
+84
View File
@@ -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' };
}
}
};
+158
View File
@@ -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;
}
};
+82
View File
@@ -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' };
}
}
};
+64
View File
@@ -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' };
}
}
}
+48
View File
@@ -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' };
}
}
}
+19
View File
@@ -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
};
+211
View File
@@ -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();
});
};
+71
View File
@@ -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;
+71
View File
@@ -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;
+6
View File
@@ -0,0 +1,6 @@
{
"ignore": [
"api/data/",
"data"
]
}
+9931
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -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"
}
}
+33
View File
@@ -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.');
}
};
+98
View File
@@ -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);
}
});
+148
View File
@@ -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;
+190
View File
@@ -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;
+198
View File
@@ -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;
+57
View File
@@ -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 };
+44
View File
@@ -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;
+55
View File
@@ -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;
+67
View File
@@ -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;
+96
View File
@@ -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 };
+9
View File
@@ -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 };
+10
View File
@@ -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;
+14
View File
@@ -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;
+123
View File
@@ -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;
-21
View File
@@ -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}}
}
-21
View File
@@ -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}}
}
-15
View File
@@ -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}}
}
-27
View File
@@ -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}}
}
-5
View File
@@ -1,5 +0,0 @@
{
"version": "1",
"name": "Raven",
"type": "collection"
}
-8
View File
@@ -1,8 +0,0 @@
vars {
url: http://localhost
port: 8000
api_key: a0fa913c5571c73
}
vars:secret [
api_secret
]
-16
View File
@@ -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
}
-3
View File
@@ -1,3 +0,0 @@
ios/
android/
.expo/
-30
View File
@@ -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>
-85
View File
@@ -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"
}
}
-32
View File
@@ -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