nsarrazin HF staff coyotte508 HF staff victor HF staff commited on
Commit
0c4cf03
1 Parent(s): 002f606

Add initial support for web browsing (#237)

Browse files

* basic poc for web search

* * Hide feature when serpapi_key is not defined
* handle error case where serpapi failed
* only use user queries for generating the query

* Update src/lib/buildPrompt.ts

Co-authored-by: Eliott C. <[email protected]>

* Update src/routes/+layout.server.ts

Co-authored-by: Eliott C. <[email protected]>

* refactored getQueryFromPrompt

* add jsdom to package.json

* begin work on fetching webpage content

* Update .env

Co-authored-by: Eliott C. <[email protected]>

* prettier fix

* Add feature for scraping webpages

* refactored search functionality
- now gets triggered from a separate endpoint
- results are stored in db
- results can be displayed in their own endpoint

* Added a stream to send updates from backend on web-search endpoint

* made stream more reliable

* Add front-end to web search feature

* made sure the web results button appears on newly posted messages

* close modal when message is done generating

* removed log statements

* Add button to open modal on loading messages too

* replace modal by collapsable menu

* make sure shared conversations also show search details

* Use spinner for collapse menu

* Fix alignment of "stop generating" button

* Fix loading indicators
- spinner only shows when web search is searching
- text loader shows after the web search is done

* fix loading icon when web search is disabled

* Update search messages & clean up summary string

* Fix alignment of timeline

* Use existing switch

* Add a background to tooltip & center it

* fix like making search messages disappear

* use correct spinner

* fix state issues

* lint

* fix bug with empty search messages

* fix like bug ?

* fix modal bug

* error handling

* fix like bug

* slice scraped text so it fits in context

* misc UI

* bottom buttons

simplify and fix

* made sure snap scrolling also works on web search updates

* loader

* margin

* remove unused function

* linter

* quickfix duplicate websearch

---------

Co-authored-by: Eliott C. <[email protected]>
Co-authored-by: Victor Mustar <[email protected]>

.env CHANGED
@@ -8,6 +8,9 @@ MONGODB_DIRECT_CONNECTION=false
8
  COOKIE_NAME=hf-chat
9
  HF_ACCESS_TOKEN=#hf_<token> from from https://huggingface.co/settings/token
10
 
 
 
 
11
  # Parameters to enable "Sign in with HF"
12
  OPENID_CLIENT_ID=
13
  OPENID_CLIENT_SECRET=
 
8
  COOKIE_NAME=hf-chat
9
  HF_ACCESS_TOKEN=#hf_<token> from from https://huggingface.co/settings/token
10
 
11
+ # used to activate search with web functionality. disabled if not defined
12
+ SERPAPI_KEY=#your serpapi key here
13
+
14
  # Parameters to enable "Sign in with HF"
15
  OPENID_CLIENT_ID=
16
  OPENID_CLIENT_SECRET=
package-lock.json CHANGED
@@ -14,12 +14,14 @@
14
  "date-fns": "^2.29.3",
15
  "dotenv": "^16.0.3",
16
  "highlight.js": "^11.7.0",
 
17
  "marked": "^4.3.0",
18
  "mongodb": "^5.3.0",
19
  "nanoid": "^4.0.2",
20
  "openid-client": "^5.4.2",
21
  "parquetjs": "^0.11.2",
22
  "postcss": "^8.4.21",
 
23
  "tailwind-scrollbar": "^3.0.0",
24
  "tailwindcss": "^3.3.1",
25
  "zod": "^3.21.4"
@@ -30,6 +32,7 @@
30
  "@sveltejs/adapter-node": "^1.2.4",
31
  "@sveltejs/kit": "^1.15.10",
32
  "@tailwindcss/typography": "^0.5.9",
 
33
  "@types/marked": "^4.0.8",
34
  "@types/parquetjs": "^0.10.3",
35
  "@typescript-eslint/eslint-plugin": "^5.45.0",
@@ -890,6 +893,14 @@
890
  "node": ">=4"
891
  }
892
  },
 
 
 
 
 
 
 
 
893
  "node_modules/@types/chai": {
894
  "version": "4.3.5",
895
  "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
@@ -917,6 +928,17 @@
917
  "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
918
  "dev": true
919
  },
 
 
 
 
 
 
 
 
 
 
 
920
  "node_modules/@types/json-schema": {
921
  "version": "7.0.11",
922
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@@ -970,6 +992,12 @@
970
  "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
971
  "dev": true
972
  },
 
 
 
 
 
 
973
  "node_modules/@types/webidl-conversions": {
974
  "version": "7.0.0",
975
  "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -1268,6 +1296,11 @@
1268
  "url": "https://opencollective.com/vitest"
1269
  }
1270
  },
 
 
 
 
 
1271
  "node_modules/acorn": {
1272
  "version": "8.8.2",
1273
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@@ -1298,6 +1331,17 @@
1298
  "node": ">=0.4.0"
1299
  }
1300
  },
 
 
 
 
 
 
 
 
 
 
 
1301
  "node_modules/ajv": {
1302
  "version": "6.12.6",
1303
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1384,6 +1428,11 @@
1384
  "node": "*"
1385
  }
1386
  },
 
 
 
 
 
1387
  "node_modules/autoprefixer": {
1388
  "version": "10.4.14",
1389
  "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@@ -1548,7 +1597,6 @@
1548
  "version": "1.6.0",
1549
  "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
1550
  "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
1551
- "dev": true,
1552
  "dependencies": {
1553
  "streamsearch": "^1.1.0"
1554
  },
@@ -1698,6 +1746,17 @@
1698
  "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1699
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
1700
  },
 
 
 
 
 
 
 
 
 
 
 
1701
  "node_modules/commander": {
1702
  "version": "4.1.1",
1703
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1770,6 +1829,53 @@
1770
  "node": ">=4"
1771
  }
1772
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1773
  "node_modules/date-fns": {
1774
  "version": "2.29.3",
1775
  "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
@@ -1798,7 +1904,6 @@
1798
  "version": "4.3.4",
1799
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1800
  "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1801
- "dev": true,
1802
  "dependencies": {
1803
  "ms": "2.1.2"
1804
  },
@@ -1811,6 +1916,11 @@
1811
  }
1812
  }
1813
  },
 
 
 
 
 
1814
  "node_modules/deep-eql": {
1815
  "version": "4.1.3",
1816
  "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
@@ -1838,6 +1948,14 @@
1838
  "node": ">=0.10.0"
1839
  }
1840
  },
 
 
 
 
 
 
 
 
1841
  "node_modules/detect-indent": {
1842
  "version": "6.1.0",
1843
  "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@@ -1887,6 +2005,17 @@
1887
  "node": ">=6.0.0"
1888
  }
1889
  },
 
 
 
 
 
 
 
 
 
 
 
1890
  "node_modules/dotenv": {
1891
  "version": "16.0.3",
1892
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
@@ -1900,6 +2029,17 @@
1900
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
1901
  "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw=="
1902
  },
 
 
 
 
 
 
 
 
 
 
 
1903
  "node_modules/es6-promise": {
1904
  "version": "3.3.1",
1905
  "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@@ -2366,6 +2506,19 @@
2366
  "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
2367
  "dev": true
2368
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2369
  "node_modules/fraction.js": {
2370
  "version": "4.2.0",
2371
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@@ -2545,6 +2698,42 @@
2545
  "node": ">=12.0.0"
2546
  }
2547
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2548
  "node_modules/human-signals": {
2549
  "version": "2.1.0",
2550
  "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -2554,6 +2743,17 @@
2554
  "node": ">=10.17.0"
2555
  }
2556
  },
 
 
 
 
 
 
 
 
 
 
 
2557
  "node_modules/ignore": {
2558
  "version": "5.2.4",
2559
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -2691,6 +2891,11 @@
2691
  "node": ">=8"
2692
  }
2693
  },
 
 
 
 
 
2694
  "node_modules/is-reference": {
2695
  "version": "1.2.1",
2696
  "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -2765,6 +2970,70 @@
2765
  "js-yaml": "bin/js-yaml.js"
2766
  }
2767
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2768
  "node_modules/json-schema-traverse": {
2769
  "version": "0.4.1",
2770
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -2990,6 +3259,25 @@
2990
  "node": ">=10.0.0"
2991
  }
2992
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2993
  "node_modules/mimic-fn": {
2994
  "version": "2.1.0",
2995
  "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -3114,8 +3402,7 @@
3114
  "node_modules/ms": {
3115
  "version": "2.1.2",
3116
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
3117
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
3118
- "dev": true
3119
  },
3120
  "node_modules/mz": {
3121
  "version": "2.7.0",
@@ -3194,6 +3481,11 @@
3194
  "node": ">=8"
3195
  }
3196
  },
 
 
 
 
 
3197
  "node_modules/object-assign": {
3198
  "version": "4.1.1",
3199
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3358,6 +3650,17 @@
3358
  "node": ">=0.6.19"
3359
  }
3360
  },
 
 
 
 
 
 
 
 
 
 
 
3361
  "node_modules/path-exists": {
3362
  "version": "4.0.0",
3363
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3747,6 +4050,11 @@
3747
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
3748
  }
3749
  },
 
 
 
 
 
3750
  "node_modules/punycode": {
3751
  "version": "2.3.0",
3752
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -3764,6 +4072,11 @@
3764
  "teleport": ">=0.2.0"
3765
  }
3766
  },
 
 
 
 
 
3767
  "node_modules/queue-microtask": {
3768
  "version": "1.2.3",
3769
  "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3831,6 +4144,11 @@
3831
  "url": "https://github.com/sponsors/mysticatea"
3832
  }
3833
  },
 
 
 
 
 
3834
  "node_modules/resolve": {
3835
  "version": "1.22.1",
3836
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -3896,6 +4214,11 @@
3896
  "fsevents": "~2.3.2"
3897
  }
3898
  },
 
 
 
 
 
3899
  "node_modules/run-parallel": {
3900
  "version": "1.2.0",
3901
  "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -3930,6 +4253,11 @@
3930
  "node": ">=6"
3931
  }
3932
  },
 
 
 
 
 
3933
  "node_modules/sander": {
3934
  "version": "0.5.1",
3935
  "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
@@ -3966,6 +4294,17 @@
3966
  "node": ">=6"
3967
  }
3968
  },
 
 
 
 
 
 
 
 
 
 
 
3969
  "node_modules/semver": {
3970
  "version": "7.3.8",
3971
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@@ -3981,6 +4320,14 @@
3981
  "node": ">=10"
3982
  }
3983
  },
 
 
 
 
 
 
 
 
3984
  "node_modules/set-cookie-parser": {
3985
  "version": "2.6.0",
3986
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
@@ -4118,7 +4465,6 @@
4118
  "version": "1.1.0",
4119
  "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
4120
  "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
4121
- "dev": true,
4122
  "engines": {
4123
  "node": ">=10.0.0"
4124
  }
@@ -4423,6 +4769,11 @@
4423
  "node": ">=12"
4424
  }
4425
  },
 
 
 
 
 
4426
  "node_modules/tailwind-scrollbar": {
4427
  "version": "3.0.0",
4428
  "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.0.0.tgz",
@@ -4576,6 +4927,20 @@
4576
  "node": ">=6"
4577
  }
4578
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4579
  "node_modules/tr46": {
4580
  "version": "3.0.0",
4581
  "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
@@ -4675,7 +5040,6 @@
4675
  "version": "5.22.0",
4676
  "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
4677
  "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
4678
- "dev": true,
4679
  "dependencies": {
4680
  "busboy": "^1.6.0"
4681
  },
@@ -4683,6 +5047,14 @@
4683
  "node": ">=14.0"
4684
  }
4685
  },
 
 
 
 
 
 
 
 
4686
  "node_modules/unplugin": {
4687
  "version": "1.3.1",
4688
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
@@ -4767,6 +5139,15 @@
4767
  "punycode": "^2.1.0"
4768
  }
4769
  },
 
 
 
 
 
 
 
 
 
4770
  "node_modules/util-deprecate": {
4771
  "version": "1.0.2",
4772
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4940,6 +5321,17 @@
4940
  }
4941
  }
4942
  },
 
 
 
 
 
 
 
 
 
 
 
4943
  "node_modules/webidl-conversions": {
4944
  "version": "7.0.0",
4945
  "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -4972,6 +5364,25 @@
4972
  "node": ">=6"
4973
  }
4974
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4975
  "node_modules/whatwg-url": {
4976
  "version": "11.0.0",
4977
  "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
@@ -5049,6 +5460,19 @@
5049
  }
5050
  }
5051
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5052
  "node_modules/yallist": {
5053
  "version": "4.0.0",
5054
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
 
14
  "date-fns": "^2.29.3",
15
  "dotenv": "^16.0.3",
16
  "highlight.js": "^11.7.0",
17
+ "jsdom": "^22.0.0",
18
  "marked": "^4.3.0",
19
  "mongodb": "^5.3.0",
20
  "nanoid": "^4.0.2",
21
  "openid-client": "^5.4.2",
22
  "parquetjs": "^0.11.2",
23
  "postcss": "^8.4.21",
24
+ "serpapi": "^1.1.1",
25
  "tailwind-scrollbar": "^3.0.0",
26
  "tailwindcss": "^3.3.1",
27
  "zod": "^3.21.4"
 
32
  "@sveltejs/adapter-node": "^1.2.4",
33
  "@sveltejs/kit": "^1.15.10",
34
  "@tailwindcss/typography": "^0.5.9",
35
+ "@types/jsdom": "^21.1.1",
36
  "@types/marked": "^4.0.8",
37
  "@types/parquetjs": "^0.10.3",
38
  "@typescript-eslint/eslint-plugin": "^5.45.0",
 
893
  "node": ">=4"
894
  }
895
  },
896
+ "node_modules/@tootallnate/once": {
897
+ "version": "2.0.0",
898
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
899
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
900
+ "engines": {
901
+ "node": ">= 10"
902
+ }
903
+ },
904
  "node_modules/@types/chai": {
905
  "version": "4.3.5",
906
  "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
 
928
  "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
929
  "dev": true
930
  },
931
+ "node_modules/@types/jsdom": {
932
+ "version": "21.1.1",
933
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.1.tgz",
934
+ "integrity": "sha512-cZFuoVLtzKP3gmq9eNosUL1R50U+USkbLtUQ1bYVgl/lKp0FZM7Cq4aIHAL8oIvQ17uSHi7jXPtfDOdjPwBE7A==",
935
+ "dev": true,
936
+ "dependencies": {
937
+ "@types/node": "*",
938
+ "@types/tough-cookie": "*",
939
+ "parse5": "^7.0.0"
940
+ }
941
+ },
942
  "node_modules/@types/json-schema": {
943
  "version": "7.0.11",
944
  "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
 
992
  "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
993
  "dev": true
994
  },
995
+ "node_modules/@types/tough-cookie": {
996
+ "version": "4.0.2",
997
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
998
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
999
+ "dev": true
1000
+ },
1001
  "node_modules/@types/webidl-conversions": {
1002
  "version": "7.0.0",
1003
  "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
 
1296
  "url": "https://opencollective.com/vitest"
1297
  }
1298
  },
1299
+ "node_modules/abab": {
1300
+ "version": "2.0.6",
1301
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
1302
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
1303
+ },
1304
  "node_modules/acorn": {
1305
  "version": "8.8.2",
1306
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
 
1331
  "node": ">=0.4.0"
1332
  }
1333
  },
1334
+ "node_modules/agent-base": {
1335
+ "version": "6.0.2",
1336
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
1337
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
1338
+ "dependencies": {
1339
+ "debug": "4"
1340
+ },
1341
+ "engines": {
1342
+ "node": ">= 6.0.0"
1343
+ }
1344
+ },
1345
  "node_modules/ajv": {
1346
  "version": "6.12.6",
1347
  "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
 
1428
  "node": "*"
1429
  }
1430
  },
1431
+ "node_modules/asynckit": {
1432
+ "version": "0.4.0",
1433
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1434
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
1435
+ },
1436
  "node_modules/autoprefixer": {
1437
  "version": "10.4.14",
1438
  "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
 
1597
  "version": "1.6.0",
1598
  "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
1599
  "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
 
1600
  "dependencies": {
1601
  "streamsearch": "^1.1.0"
1602
  },
 
1746
  "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1747
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
1748
  },
1749
+ "node_modules/combined-stream": {
1750
+ "version": "1.0.8",
1751
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1752
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1753
+ "dependencies": {
1754
+ "delayed-stream": "~1.0.0"
1755
+ },
1756
+ "engines": {
1757
+ "node": ">= 0.8"
1758
+ }
1759
+ },
1760
  "node_modules/commander": {
1761
  "version": "4.1.1",
1762
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
 
1829
  "node": ">=4"
1830
  }
1831
  },
1832
+ "node_modules/cssstyle": {
1833
+ "version": "3.0.0",
1834
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz",
1835
+ "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==",
1836
+ "dependencies": {
1837
+ "rrweb-cssom": "^0.6.0"
1838
+ },
1839
+ "engines": {
1840
+ "node": ">=14"
1841
+ }
1842
+ },
1843
+ "node_modules/data-urls": {
1844
+ "version": "4.0.0",
1845
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
1846
+ "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==",
1847
+ "dependencies": {
1848
+ "abab": "^2.0.6",
1849
+ "whatwg-mimetype": "^3.0.0",
1850
+ "whatwg-url": "^12.0.0"
1851
+ },
1852
+ "engines": {
1853
+ "node": ">=14"
1854
+ }
1855
+ },
1856
+ "node_modules/data-urls/node_modules/tr46": {
1857
+ "version": "4.1.1",
1858
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
1859
+ "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
1860
+ "dependencies": {
1861
+ "punycode": "^2.3.0"
1862
+ },
1863
+ "engines": {
1864
+ "node": ">=14"
1865
+ }
1866
+ },
1867
+ "node_modules/data-urls/node_modules/whatwg-url": {
1868
+ "version": "12.0.1",
1869
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz",
1870
+ "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==",
1871
+ "dependencies": {
1872
+ "tr46": "^4.1.1",
1873
+ "webidl-conversions": "^7.0.0"
1874
+ },
1875
+ "engines": {
1876
+ "node": ">=14"
1877
+ }
1878
+ },
1879
  "node_modules/date-fns": {
1880
  "version": "2.29.3",
1881
  "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
 
1904
  "version": "4.3.4",
1905
  "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1906
  "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
 
1907
  "dependencies": {
1908
  "ms": "2.1.2"
1909
  },
 
1916
  }
1917
  }
1918
  },
1919
+ "node_modules/decimal.js": {
1920
+ "version": "10.4.3",
1921
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
1922
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
1923
+ },
1924
  "node_modules/deep-eql": {
1925
  "version": "4.1.3",
1926
  "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
 
1948
  "node": ">=0.10.0"
1949
  }
1950
  },
1951
+ "node_modules/delayed-stream": {
1952
+ "version": "1.0.0",
1953
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1954
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1955
+ "engines": {
1956
+ "node": ">=0.4.0"
1957
+ }
1958
+ },
1959
  "node_modules/detect-indent": {
1960
  "version": "6.1.0",
1961
  "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
 
2005
  "node": ">=6.0.0"
2006
  }
2007
  },
2008
+ "node_modules/domexception": {
2009
+ "version": "4.0.0",
2010
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
2011
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
2012
+ "dependencies": {
2013
+ "webidl-conversions": "^7.0.0"
2014
+ },
2015
+ "engines": {
2016
+ "node": ">=12"
2017
+ }
2018
+ },
2019
  "node_modules/dotenv": {
2020
  "version": "16.0.3",
2021
  "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
 
2029
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz",
2030
  "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw=="
2031
  },
2032
+ "node_modules/entities": {
2033
+ "version": "4.5.0",
2034
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
2035
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
2036
+ "engines": {
2037
+ "node": ">=0.12"
2038
+ },
2039
+ "funding": {
2040
+ "url": "https://github.com/fb55/entities?sponsor=1"
2041
+ }
2042
+ },
2043
  "node_modules/es6-promise": {
2044
  "version": "3.3.1",
2045
  "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
 
2506
  "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
2507
  "dev": true
2508
  },
2509
+ "node_modules/form-data": {
2510
+ "version": "4.0.0",
2511
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
2512
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
2513
+ "dependencies": {
2514
+ "asynckit": "^0.4.0",
2515
+ "combined-stream": "^1.0.8",
2516
+ "mime-types": "^2.1.12"
2517
+ },
2518
+ "engines": {
2519
+ "node": ">= 6"
2520
+ }
2521
+ },
2522
  "node_modules/fraction.js": {
2523
  "version": "4.2.0",
2524
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
 
2698
  "node": ">=12.0.0"
2699
  }
2700
  },
2701
+ "node_modules/html-encoding-sniffer": {
2702
+ "version": "3.0.0",
2703
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
2704
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
2705
+ "dependencies": {
2706
+ "whatwg-encoding": "^2.0.0"
2707
+ },
2708
+ "engines": {
2709
+ "node": ">=12"
2710
+ }
2711
+ },
2712
+ "node_modules/http-proxy-agent": {
2713
+ "version": "5.0.0",
2714
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
2715
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
2716
+ "dependencies": {
2717
+ "@tootallnate/once": "2",
2718
+ "agent-base": "6",
2719
+ "debug": "4"
2720
+ },
2721
+ "engines": {
2722
+ "node": ">= 6"
2723
+ }
2724
+ },
2725
+ "node_modules/https-proxy-agent": {
2726
+ "version": "5.0.1",
2727
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
2728
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
2729
+ "dependencies": {
2730
+ "agent-base": "6",
2731
+ "debug": "4"
2732
+ },
2733
+ "engines": {
2734
+ "node": ">= 6"
2735
+ }
2736
+ },
2737
  "node_modules/human-signals": {
2738
  "version": "2.1.0",
2739
  "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
 
2743
  "node": ">=10.17.0"
2744
  }
2745
  },
2746
+ "node_modules/iconv-lite": {
2747
+ "version": "0.6.3",
2748
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
2749
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
2750
+ "dependencies": {
2751
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
2752
+ },
2753
+ "engines": {
2754
+ "node": ">=0.10.0"
2755
+ }
2756
+ },
2757
  "node_modules/ignore": {
2758
  "version": "5.2.4",
2759
  "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
 
2891
  "node": ">=8"
2892
  }
2893
  },
2894
+ "node_modules/is-potential-custom-element-name": {
2895
+ "version": "1.0.1",
2896
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
2897
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
2898
+ },
2899
  "node_modules/is-reference": {
2900
  "version": "1.2.1",
2901
  "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
 
2970
  "js-yaml": "bin/js-yaml.js"
2971
  }
2972
  },
2973
+ "node_modules/jsdom": {
2974
+ "version": "22.0.0",
2975
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz",
2976
+ "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==",
2977
+ "dependencies": {
2978
+ "abab": "^2.0.6",
2979
+ "cssstyle": "^3.0.0",
2980
+ "data-urls": "^4.0.0",
2981
+ "decimal.js": "^10.4.3",
2982
+ "domexception": "^4.0.0",
2983
+ "form-data": "^4.0.0",
2984
+ "html-encoding-sniffer": "^3.0.0",
2985
+ "http-proxy-agent": "^5.0.0",
2986
+ "https-proxy-agent": "^5.0.1",
2987
+ "is-potential-custom-element-name": "^1.0.1",
2988
+ "nwsapi": "^2.2.4",
2989
+ "parse5": "^7.1.2",
2990
+ "rrweb-cssom": "^0.6.0",
2991
+ "saxes": "^6.0.0",
2992
+ "symbol-tree": "^3.2.4",
2993
+ "tough-cookie": "^4.1.2",
2994
+ "w3c-xmlserializer": "^4.0.0",
2995
+ "webidl-conversions": "^7.0.0",
2996
+ "whatwg-encoding": "^2.0.0",
2997
+ "whatwg-mimetype": "^3.0.0",
2998
+ "whatwg-url": "^12.0.1",
2999
+ "ws": "^8.13.0",
3000
+ "xml-name-validator": "^4.0.0"
3001
+ },
3002
+ "engines": {
3003
+ "node": ">=16"
3004
+ },
3005
+ "peerDependencies": {
3006
+ "canvas": "^2.5.0"
3007
+ },
3008
+ "peerDependenciesMeta": {
3009
+ "canvas": {
3010
+ "optional": true
3011
+ }
3012
+ }
3013
+ },
3014
+ "node_modules/jsdom/node_modules/tr46": {
3015
+ "version": "4.1.1",
3016
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
3017
+ "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
3018
+ "dependencies": {
3019
+ "punycode": "^2.3.0"
3020
+ },
3021
+ "engines": {
3022
+ "node": ">=14"
3023
+ }
3024
+ },
3025
+ "node_modules/jsdom/node_modules/whatwg-url": {
3026
+ "version": "12.0.1",
3027
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz",
3028
+ "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==",
3029
+ "dependencies": {
3030
+ "tr46": "^4.1.1",
3031
+ "webidl-conversions": "^7.0.0"
3032
+ },
3033
+ "engines": {
3034
+ "node": ">=14"
3035
+ }
3036
+ },
3037
  "node_modules/json-schema-traverse": {
3038
  "version": "0.4.1",
3039
  "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
 
3259
  "node": ">=10.0.0"
3260
  }
3261
  },
3262
+ "node_modules/mime-db": {
3263
+ "version": "1.52.0",
3264
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
3265
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
3266
+ "engines": {
3267
+ "node": ">= 0.6"
3268
+ }
3269
+ },
3270
+ "node_modules/mime-types": {
3271
+ "version": "2.1.35",
3272
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
3273
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
3274
+ "dependencies": {
3275
+ "mime-db": "1.52.0"
3276
+ },
3277
+ "engines": {
3278
+ "node": ">= 0.6"
3279
+ }
3280
+ },
3281
  "node_modules/mimic-fn": {
3282
  "version": "2.1.0",
3283
  "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
 
3402
  "node_modules/ms": {
3403
  "version": "2.1.2",
3404
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
3405
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 
3406
  },
3407
  "node_modules/mz": {
3408
  "version": "2.7.0",
 
3481
  "node": ">=8"
3482
  }
3483
  },
3484
+ "node_modules/nwsapi": {
3485
+ "version": "2.2.4",
3486
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz",
3487
+ "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g=="
3488
+ },
3489
  "node_modules/object-assign": {
3490
  "version": "4.1.1",
3491
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 
3650
  "node": ">=0.6.19"
3651
  }
3652
  },
3653
+ "node_modules/parse5": {
3654
+ "version": "7.1.2",
3655
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
3656
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
3657
+ "dependencies": {
3658
+ "entities": "^4.4.0"
3659
+ },
3660
+ "funding": {
3661
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
3662
+ }
3663
+ },
3664
  "node_modules/path-exists": {
3665
  "version": "4.0.0",
3666
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
4050
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
4051
  }
4052
  },
4053
+ "node_modules/psl": {
4054
+ "version": "1.9.0",
4055
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
4056
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
4057
+ },
4058
  "node_modules/punycode": {
4059
  "version": "2.3.0",
4060
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
 
4072
  "teleport": ">=0.2.0"
4073
  }
4074
  },
4075
+ "node_modules/querystringify": {
4076
+ "version": "2.2.0",
4077
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
4078
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
4079
+ },
4080
  "node_modules/queue-microtask": {
4081
  "version": "1.2.3",
4082
  "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 
4144
  "url": "https://github.com/sponsors/mysticatea"
4145
  }
4146
  },
4147
+ "node_modules/requires-port": {
4148
+ "version": "1.0.0",
4149
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
4150
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
4151
+ },
4152
  "node_modules/resolve": {
4153
  "version": "1.22.1",
4154
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
 
4214
  "fsevents": "~2.3.2"
4215
  }
4216
  },
4217
+ "node_modules/rrweb-cssom": {
4218
+ "version": "0.6.0",
4219
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
4220
+ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw=="
4221
+ },
4222
  "node_modules/run-parallel": {
4223
  "version": "1.2.0",
4224
  "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 
4253
  "node": ">=6"
4254
  }
4255
  },
4256
+ "node_modules/safer-buffer": {
4257
+ "version": "2.1.2",
4258
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
4259
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
4260
+ },
4261
  "node_modules/sander": {
4262
  "version": "0.5.1",
4263
  "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
 
4294
  "node": ">=6"
4295
  }
4296
  },
4297
+ "node_modules/saxes": {
4298
+ "version": "6.0.0",
4299
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
4300
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
4301
+ "dependencies": {
4302
+ "xmlchars": "^2.2.0"
4303
+ },
4304
+ "engines": {
4305
+ "node": ">=v12.22.7"
4306
+ }
4307
+ },
4308
  "node_modules/semver": {
4309
  "version": "7.3.8",
4310
  "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
 
4320
  "node": ">=10"
4321
  }
4322
  },
4323
+ "node_modules/serpapi": {
4324
+ "version": "1.1.1",
4325
+ "resolved": "https://registry.npmjs.org/serpapi/-/serpapi-1.1.1.tgz",
4326
+ "integrity": "sha512-t5Bqu/6VMJ9naX8K+qCgUStpZOaNQFvIM4AudhMJLS6sqQT/EHaYrhGidDZHVx8QvcEdY6y1wNlxizOCtvJtUQ==",
4327
+ "dependencies": {
4328
+ "undici": "^5.12.0"
4329
+ }
4330
+ },
4331
  "node_modules/set-cookie-parser": {
4332
  "version": "2.6.0",
4333
  "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
 
4465
  "version": "1.1.0",
4466
  "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
4467
  "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
 
4468
  "engines": {
4469
  "node": ">=10.0.0"
4470
  }
 
4769
  "node": ">=12"
4770
  }
4771
  },
4772
+ "node_modules/symbol-tree": {
4773
+ "version": "3.2.4",
4774
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
4775
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
4776
+ },
4777
  "node_modules/tailwind-scrollbar": {
4778
  "version": "3.0.0",
4779
  "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.0.0.tgz",
 
4927
  "node": ">=6"
4928
  }
4929
  },
4930
+ "node_modules/tough-cookie": {
4931
+ "version": "4.1.2",
4932
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
4933
+ "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
4934
+ "dependencies": {
4935
+ "psl": "^1.1.33",
4936
+ "punycode": "^2.1.1",
4937
+ "universalify": "^0.2.0",
4938
+ "url-parse": "^1.5.3"
4939
+ },
4940
+ "engines": {
4941
+ "node": ">=6"
4942
+ }
4943
+ },
4944
  "node_modules/tr46": {
4945
  "version": "3.0.0",
4946
  "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
 
5040
  "version": "5.22.0",
5041
  "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz",
5042
  "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==",
 
5043
  "dependencies": {
5044
  "busboy": "^1.6.0"
5045
  },
 
5047
  "node": ">=14.0"
5048
  }
5049
  },
5050
+ "node_modules/universalify": {
5051
+ "version": "0.2.0",
5052
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
5053
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
5054
+ "engines": {
5055
+ "node": ">= 4.0.0"
5056
+ }
5057
+ },
5058
  "node_modules/unplugin": {
5059
  "version": "1.3.1",
5060
  "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.3.1.tgz",
 
5139
  "punycode": "^2.1.0"
5140
  }
5141
  },
5142
+ "node_modules/url-parse": {
5143
+ "version": "1.5.10",
5144
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
5145
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
5146
+ "dependencies": {
5147
+ "querystringify": "^2.1.1",
5148
+ "requires-port": "^1.0.0"
5149
+ }
5150
+ },
5151
  "node_modules/util-deprecate": {
5152
  "version": "1.0.2",
5153
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 
5321
  }
5322
  }
5323
  },
5324
+ "node_modules/w3c-xmlserializer": {
5325
+ "version": "4.0.0",
5326
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
5327
+ "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
5328
+ "dependencies": {
5329
+ "xml-name-validator": "^4.0.0"
5330
+ },
5331
+ "engines": {
5332
+ "node": ">=14"
5333
+ }
5334
+ },
5335
  "node_modules/webidl-conversions": {
5336
  "version": "7.0.0",
5337
  "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
 
5364
  "node": ">=6"
5365
  }
5366
  },
5367
+ "node_modules/whatwg-encoding": {
5368
+ "version": "2.0.0",
5369
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
5370
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
5371
+ "dependencies": {
5372
+ "iconv-lite": "0.6.3"
5373
+ },
5374
+ "engines": {
5375
+ "node": ">=12"
5376
+ }
5377
+ },
5378
+ "node_modules/whatwg-mimetype": {
5379
+ "version": "3.0.0",
5380
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
5381
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
5382
+ "engines": {
5383
+ "node": ">=12"
5384
+ }
5385
+ },
5386
  "node_modules/whatwg-url": {
5387
  "version": "11.0.0",
5388
  "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
 
5460
  }
5461
  }
5462
  },
5463
+ "node_modules/xml-name-validator": {
5464
+ "version": "4.0.0",
5465
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
5466
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
5467
+ "engines": {
5468
+ "node": ">=12"
5469
+ }
5470
+ },
5471
+ "node_modules/xmlchars": {
5472
+ "version": "2.2.0",
5473
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
5474
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
5475
+ },
5476
  "node_modules/yallist": {
5477
  "version": "4.0.0",
5478
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
package.json CHANGED
@@ -19,6 +19,7 @@
19
  "@sveltejs/adapter-node": "^1.2.4",
20
  "@sveltejs/kit": "^1.15.10",
21
  "@tailwindcss/typography": "^0.5.9",
 
22
  "@types/marked": "^4.0.8",
23
  "@types/parquetjs": "^0.10.3",
24
  "@typescript-eslint/eslint-plugin": "^5.45.0",
@@ -45,12 +46,14 @@
45
  "date-fns": "^2.29.3",
46
  "dotenv": "^16.0.3",
47
  "highlight.js": "^11.7.0",
 
48
  "marked": "^4.3.0",
49
  "mongodb": "^5.3.0",
50
  "nanoid": "^4.0.2",
51
  "openid-client": "^5.4.2",
52
  "parquetjs": "^0.11.2",
53
  "postcss": "^8.4.21",
 
54
  "tailwind-scrollbar": "^3.0.0",
55
  "tailwindcss": "^3.3.1",
56
  "zod": "^3.21.4"
 
19
  "@sveltejs/adapter-node": "^1.2.4",
20
  "@sveltejs/kit": "^1.15.10",
21
  "@tailwindcss/typography": "^0.5.9",
22
+ "@types/jsdom": "^21.1.1",
23
  "@types/marked": "^4.0.8",
24
  "@types/parquetjs": "^0.10.3",
25
  "@typescript-eslint/eslint-plugin": "^5.45.0",
 
46
  "date-fns": "^2.29.3",
47
  "dotenv": "^16.0.3",
48
  "highlight.js": "^11.7.0",
49
+ "jsdom": "^22.0.0",
50
  "marked": "^4.3.0",
51
  "mongodb": "^5.3.0",
52
  "nanoid": "^4.0.2",
53
  "openid-client": "^5.4.2",
54
  "parquetjs": "^0.11.2",
55
  "postcss": "^8.4.21",
56
+ "serpapi": "^1.1.1",
57
  "tailwind-scrollbar": "^3.0.0",
58
  "tailwindcss": "^3.3.1",
59
  "zod": "^3.21.4"
src/lib/buildPrompt.ts CHANGED
@@ -1,15 +1,18 @@
1
  import type { BackendModel } from "./server/models";
2
  import type { Message } from "./types/Message";
3
-
 
4
  /**
5
  * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
6
  *
7
  * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
8
  */
9
- export function buildPrompt(
 
10
  messages: Pick<Message, "from" | "content">[],
11
- model: BackendModel
12
- ): string {
 
13
  const prompt =
14
  messages
15
  .map(
@@ -25,12 +28,30 @@ export function buildPrompt(
25
  )
26
  .join("") + model.assistantMessageToken;
27
 
28
- // Not super precise, but it's truncated in the model's backend anyway
29
- return (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  model.preprompt +
 
31
  prompt
32
  .split(" ")
33
  .slice(-(model.parameters?.truncate ?? 0))
34
- .join(" ")
35
- );
 
 
36
  }
 
1
  import type { BackendModel } from "./server/models";
2
  import type { Message } from "./types/Message";
3
+ import { collections } from "$lib/server/database";
4
+ import { ObjectId } from "mongodb";
5
  /**
6
  * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
7
  *
8
  * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
9
  */
10
+
11
+ export async function buildPrompt(
12
  messages: Pick<Message, "from" | "content">[],
13
+ model: BackendModel,
14
+ webSearchId?: string
15
+ ): Promise<string> {
16
  const prompt =
17
  messages
18
  .map(
 
28
  )
29
  .join("") + model.assistantMessageToken;
30
 
31
+ let webPrompt = "";
32
+
33
+ if (webSearchId) {
34
+ const webSearch = await collections.webSearches.findOne({
35
+ _id: new ObjectId(webSearchId),
36
+ });
37
+
38
+ if (!webSearch) throw new Error("Web search not found");
39
+
40
+ if (webSearch.summary) {
41
+ webPrompt =
42
+ model.assistantMessageToken +
43
+ `The following context was found while searching the internet: ${webSearch.summary}` +
44
+ model.messageEndToken;
45
+ }
46
+ }
47
+ const finalPrompt =
48
  model.preprompt +
49
+ webPrompt +
50
  prompt
51
  .split(" ")
52
  .slice(-(model.parameters?.truncate ?? 0))
53
+ .join(" ");
54
+
55
+ // Not super precise, but it's truncated in the model's backend anyway
56
+ return finalPrompt;
57
  }
src/lib/components/OpenWebSearchResults.svelte ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
3
+ import CarbonCaretRight from "~icons/carbon/caret-right";
4
+
5
+ import CarbonCheckmark from "~icons/carbon/checkmark-filled";
6
+ import CarbonError from "~icons/carbon/error-filled";
7
+
8
+ import EosIconsLoading from "~icons/eos-icons/loading";
9
+
10
+ import { base } from "$app/paths";
11
+ import { onMount } from "svelte";
12
+
13
+ export let loading = false;
14
+ export let classNames = "";
15
+ export let webSearchId: string | undefined;
16
+ export let webSearchMessages: WebSearchMessage[] = [];
17
+
18
+ let detailsOpen: boolean;
19
+ let error: boolean;
20
+ onMount(() => {
21
+ if (webSearchMessages.length === 0 && webSearchId) {
22
+ fetch(`${base}/search/${webSearchId}`)
23
+ .then((res) => res.json())
24
+ .then((res) => {
25
+ webSearchMessages = [...res.messages, { type: "result", id: webSearchId }];
26
+ })
27
+ .catch((err) => console.log(err));
28
+ }
29
+ });
30
+ $: error = webSearchMessages.some((message) => message.type === "error");
31
+ </script>
32
+
33
+ <details
34
+ class="details flex w-fit rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 {classNames}"
35
+ on:toggle={() => {
36
+ if (webSearchMessages.length === 0 && webSearchId) {
37
+ fetch(`${base}/search/${webSearchId}`)
38
+ .then((res) => res.json())
39
+ .then((res) => {
40
+ webSearchMessages = [...res.messages, { type: "result", id: webSearchId }];
41
+ })
42
+ .catch((err) => console.log(err));
43
+ }
44
+ }}
45
+ bind:open={detailsOpen}
46
+ >
47
+ <summary
48
+ class="align-center flex cursor-pointer select-none list-none py-1 pl-2.5 pr-2 align-text-top transition-all"
49
+ >
50
+ {#if error}
51
+ <CarbonError class="my-auto text-red-700 dark:text-red-500" />
52
+ {:else if loading}
53
+ <EosIconsLoading class="my-auto text-gray-500" />
54
+ {:else}
55
+ <CarbonCheckmark class="my-auto text-gray-500" />
56
+ {/if}
57
+ <span class="px-2 font-medium" class:text-red-700={error} class:dark:text-red-500={error}
58
+ >Web search
59
+ </span>
60
+ <div class="my-auto transition-all" class:rotate-90={detailsOpen}>
61
+ <CarbonCaretRight />
62
+ </div>
63
+ </summary>
64
+
65
+ <div class="content p-5 pb-1">
66
+ {#if webSearchMessages.length === 0}
67
+ <div class="mx-auto w-fit">
68
+ <EosIconsLoading class="mb-3 h-10 w-10" />
69
+ </div>
70
+ {:else}
71
+ <ol class="relative border-l border-gray-200 dark:border-gray-600">
72
+ {#each webSearchMessages as message}
73
+ {#if message.type === "update"}
74
+ <li class="mb-4 ml-4">
75
+ <div
76
+ class="h-3 w-3 -translate-x-[1.4rem] rounded-full bg-gray-200 dark:bg-gray-600"
77
+ />
78
+ <h3 class="text-md -translate-y-[1.1rem] text-gray-800 dark:text-gray-100">
79
+ {message.message}
80
+ </h3>
81
+ {#if message.args}
82
+ <p
83
+ class="mb-4 -translate-y-[1.1rem] font-normal text-gray-500 dark:text-gray-400 "
84
+ >
85
+ {message.args}
86
+ </p>
87
+ {/if}
88
+ </li>
89
+ {:else if message.type === "error"}
90
+ <li class="mb-4 ml-4">
91
+ <div
92
+ class="h-3 w-3 -translate-x-[1.4rem] rounded-full text-red-700 dark:text-red-500"
93
+ >
94
+ <CarbonError class="h-3 w-3" />
95
+ </div>
96
+ <h3 class="text-md -translate-y-[1.1rem] text-red-700 dark:text-red-500">
97
+ {message.message}
98
+ </h3>
99
+ {#if message.args}
100
+ <p class="mb-4 -translate-y-[1.1rem] font-normal text-gray-500 dark:text-gray-400 ">
101
+ {message.args}
102
+ </p>
103
+ {/if}
104
+ </li>
105
+ {/if}
106
+ <p />
107
+ {/each}
108
+ </ol>
109
+ {/if}
110
+ </div>
111
+ </details>
112
+
113
+ <style>
114
+ @keyframes grow {
115
+ 0% {
116
+ font-size: 0;
117
+ opacity: 0;
118
+ }
119
+ 30% {
120
+ font-size: 1em;
121
+ opacity: 0;
122
+ }
123
+ 100% {
124
+ opacity: 1;
125
+ }
126
+ }
127
+
128
+ .details[open] .content {
129
+ animation-name: grow;
130
+ animation-duration: 300ms;
131
+ animation-delay: 0ms;
132
+ }
133
+ </style>
src/lib/components/StopGeneratingBtn.svelte CHANGED
@@ -1,17 +1,13 @@
1
  <script lang="ts">
2
- import CarbonPause from "~icons/carbon/pause-filled";
3
 
4
- export let visible = false;
5
- export let className = "";
6
  </script>
7
 
8
  <button
9
  type="button"
10
  on:click
11
- class="btn flex rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600
12
- {className}
13
- {visible ? 'visible opacity-100' : 'invisible opacity-0'}
14
- "
15
  >
16
- <CarbonPause class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-400" /> Stop generating
17
  </button>
 
1
  <script lang="ts">
2
+ import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
3
 
4
+ export let classNames = "";
 
5
  </script>
6
 
7
  <button
8
  type="button"
9
  on:click
10
+ class="btn flex h-9 rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600 {classNames}"
 
 
 
11
  >
12
+ <CarbonStopFilledAlt class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-400" /> Stop generating
13
  </button>
src/lib/components/Switch.svelte CHANGED
@@ -5,7 +5,9 @@
5
 
6
  <input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
7
  <div
8
- class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-black peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 peer-checked:[&>div]:translate-x-3.5"
 
 
9
  >
10
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
11
  </div>
 
5
 
6
  <input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
7
  <div
8
+ on:click
9
+ on:keypress
10
+ class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
11
  >
12
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
13
  </div>
src/lib/components/WebSearchToggle.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { webSearchParameters } from "$lib/stores/webSearchParameters";
3
+ import CarbonInformation from "~icons/carbon/information";
4
+ import Switch from "./Switch.svelte";
5
+
6
+ const toggle = () => ($webSearchParameters.useSearch = !$webSearchParameters.useSearch);
7
+ </script>
8
+
9
+ <div
10
+ class="flex h-9 cursor-pointer select-none items-center gap-2 rounded-xl border bg-white p-1.5 shadow-sm hover:shadow-none dark:border-gray-800 dark:bg-gray-900"
11
+ on:click={toggle}
12
+ on:keypress={toggle}
13
+ >
14
+ <Switch name="useSearch" bind:checked={$webSearchParameters.useSearch} on:click on:keypress />
15
+ <div class="whitespace-nowrap text-sm text-gray-800 dark:text-gray-200">Search web</div>
16
+ <div class="group relative w-max">
17
+ <CarbonInformation class="text-xs text-gray-500" />
18
+ <div
19
+ class="pointer-events-none absolute -top-20 left-1/2 w-max -translate-x-1/2 rounded-md bg-gray-100 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-800"
20
+ >
21
+ <p class="max-w-sm text-sm text-gray-800 dark:text-gray-200">
22
+ When enabled, the model will try to complement its answer with information queried from the
23
+ web.
24
+ </p>
25
+ </div>
26
+ </div>
27
+ </div>
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -13,6 +13,9 @@
13
  import CarbonThumbsDown from "~icons/carbon/thumbs-down";
14
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
15
  import type { Model } from "$lib/types/Model";
 
 
 
16
 
17
  function sanitizeMd(md: string) {
18
  let ret = md
@@ -43,6 +46,9 @@
43
  export let isAuthor = true;
44
  export let readOnly = false;
45
  export let isTapped = false;
 
 
 
46
 
47
  const dispatch = createEventDispatcher<{
48
  retry: { content: string; id: Message["id"] };
@@ -89,6 +95,13 @@
89
 
90
  $: downloadLink =
91
  message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
 
 
 
 
 
 
 
92
  </script>
93
 
94
  {#if message.from === "assistant"}
@@ -103,11 +116,22 @@
103
  class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg"
104
  />
105
  <div
106
- class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
107
  >
108
- {#if !message.content}
109
- <IconLoading classNames="absolute inset-0 m-auto" />
 
 
 
 
 
 
 
 
 
 
110
  {/if}
 
111
  <div
112
  class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
113
  bind:this={contentEl}
 
13
  import CarbonThumbsDown from "~icons/carbon/thumbs-down";
14
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
15
  import type { Model } from "$lib/types/Model";
16
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
17
+
18
+ import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
19
 
20
  function sanitizeMd(md: string) {
21
  let ret = md
 
46
  export let isAuthor = true;
47
  export let readOnly = false;
48
  export let isTapped = false;
49
+ export let isLast = false;
50
+
51
+ export let webSearchMessages: WebSearchMessage[] = [];
52
 
53
  const dispatch = createEventDispatcher<{
54
  retry: { content: string; id: Message["id"] };
 
95
 
96
  $: downloadLink =
97
  message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
98
+
99
+ let webSearchIsDone = true;
100
+
101
+ $: webSearchIsDone =
102
+ !!message.webSearchId ||
103
+ (webSearchMessages.length > 0 &&
104
+ webSearchMessages[webSearchMessages.length - 1].type === "result");
105
  </script>
106
 
107
  {#if message.from === "assistant"}
 
116
  class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg"
117
  />
118
  <div
119
+ class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[60px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
120
  >
121
+ {#if message.webSearchId || (webSearchMessages.length > 0 && isLast)}
122
+ {#key (message.webSearchId, message.score, loading)}
123
+ <OpenWebSearchResults
124
+ classNames={tokens.length ? "mb-3" : ""}
125
+ webSearchId={message.webSearchId}
126
+ {webSearchMessages}
127
+ loading={!webSearchIsDone}
128
+ />
129
+ {/key}
130
+ {/if}
131
+ {#if !message.content && (webSearchIsDone || webSearchMessages.length === 0)}
132
+ <IconLoading />
133
  {/if}
134
+
135
  <div
136
  class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
137
  bind:this={contentEl}
src/lib/components/chat/ChatMessages.svelte CHANGED
@@ -8,6 +8,8 @@
8
  import type { LayoutData } from "../../../routes/$types";
9
  import ChatIntroduction from "./ChatIntroduction.svelte";
10
  import ChatMessage from "./ChatMessage.svelte";
 
 
11
 
12
  export let messages: Message[];
13
  export let loading: boolean;
@@ -20,6 +22,8 @@
20
 
21
  let chatContainer: HTMLElement;
22
 
 
 
23
  async function scrollToBottom() {
24
  await tick();
25
  chatContainer.scrollTop = chatContainer.scrollHeight;
@@ -33,20 +37,24 @@
33
 
34
  <div
35
  class="scrollbar-custom mr-1 h-full overflow-y-auto"
36
- use:snapScrollToBottom={messages.length ? messages : false}
37
  bind:this={chatContainer}
38
  >
39
  <div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
40
  {#each messages as message, i}
41
- <ChatMessage
42
- loading={loading && i === messages.length - 1}
43
- {message}
44
- {isAuthor}
45
- {readOnly}
46
- model={currentModel}
47
- on:retry
48
- on:vote
49
- />
 
 
 
 
50
  {:else}
51
  <ChatIntroduction {settings} {models} {currentModel} on:message />
52
  {/each}
@@ -54,9 +62,11 @@
54
  <ChatMessage
55
  message={{ from: "assistant", content: "", id: randomUUID() }}
56
  model={currentModel}
 
 
57
  />
58
  {/if}
59
- <div class="h-36 flex-none" />
60
  </div>
61
  <ScrollToBottomBtn
62
  class="bottom-36 right-4 max-md:hidden lg:right-10"
 
8
  import type { LayoutData } from "../../../routes/$types";
9
  import ChatIntroduction from "./ChatIntroduction.svelte";
10
  import ChatMessage from "./ChatMessage.svelte";
11
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
12
+ import { page } from "$app/stores";
13
 
14
  export let messages: Message[];
15
  export let loading: boolean;
 
22
 
23
  let chatContainer: HTMLElement;
24
 
25
+ export let webSearchMessages: WebSearchMessage[] = [];
26
+
27
  async function scrollToBottom() {
28
  await tick();
29
  chatContainer.scrollTop = chatContainer.scrollHeight;
 
37
 
38
  <div
39
  class="scrollbar-custom mr-1 h-full overflow-y-auto"
40
+ use:snapScrollToBottom={messages.length ? [...messages, ...webSearchMessages] : false}
41
  bind:this={chatContainer}
42
  >
43
  <div class="mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
44
  {#each messages as message, i}
45
+ {#key (message.id, $page.params.id)}
46
+ <ChatMessage
47
+ loading={loading && i === messages.length - 1}
48
+ {message}
49
+ {isAuthor}
50
+ {readOnly}
51
+ model={currentModel}
52
+ {webSearchMessages}
53
+ isLast={i === messages.length - 1}
54
+ on:retry
55
+ on:vote
56
+ />
57
+ {/key}
58
  {:else}
59
  <ChatIntroduction {settings} {models} {currentModel} on:message />
60
  {/each}
 
62
  <ChatMessage
63
  message={{ from: "assistant", content: "", id: randomUUID() }}
64
  model={currentModel}
65
+ isLast={true}
66
+ {webSearchMessages}
67
  />
68
  {/if}
69
+ <div class="h-44 flex-none" />
70
  </div>
71
  <ScrollToBottomBtn
72
  class="bottom-36 right-4 max-md:hidden lg:right-10"
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -4,7 +4,7 @@
4
 
5
  import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
  import CarbonExport from "~icons/carbon/export";
7
- import CarbonPause from "~icons/carbon/pause-filled";
8
  import EosIconsLoading from "~icons/eos-icons/loading";
9
 
10
  import ChatMessages from "./ChatMessages.svelte";
@@ -12,6 +12,8 @@
12
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
13
  import type { Model } from "$lib/types/Model";
14
  import type { LayoutData } from "../../../routes/$types";
 
 
15
  import LoginModal from "../LoginModal.svelte";
16
 
17
  export let messages: Message[] = [];
@@ -21,6 +23,7 @@
21
  export let currentModel: Model;
22
  export let models: Model[];
23
  export let settings: LayoutData["settings"];
 
24
 
25
  export let loginRequired = false;
26
  $: isReadOnly = !models.some((model) => model.id === currentModel.id);
@@ -55,6 +58,7 @@
55
  {messages}
56
  readOnly={isReadOnly}
57
  isAuthor={!shared}
 
58
  on:message
59
  on:vote
60
  on:retry={(ev) => {
@@ -64,8 +68,16 @@
64
  <div
65
  class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
66
  >
67
- <div class="my-[0.5rem] hidden justify-center sm:flex">
68
- <StopGeneratingBtn visible={loading} on:click={() => dispatch("stop")} />
 
 
 
 
 
 
 
 
69
  </div>
70
  <form
71
  on:submit|preventDefault={handleSubmit}
@@ -89,7 +101,7 @@
89
  class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:hidden"
90
  on:click={() => dispatch("stop")}
91
  >
92
- <CarbonPause />
93
  </button>
94
  <div
95
  class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:flex"
 
4
 
5
  import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
  import CarbonExport from "~icons/carbon/export";
7
+ import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt";
8
  import EosIconsLoading from "~icons/eos-icons/loading";
9
 
10
  import ChatMessages from "./ChatMessages.svelte";
 
12
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
13
  import type { Model } from "$lib/types/Model";
14
  import type { LayoutData } from "../../../routes/$types";
15
+ import WebSearchToggle from "../WebSearchToggle.svelte";
16
+ import type { WebSearchMessage } from "$lib/types/WebSearch";
17
  import LoginModal from "../LoginModal.svelte";
18
 
19
  export let messages: Message[] = [];
 
23
  export let currentModel: Model;
24
  export let models: Model[];
25
  export let settings: LayoutData["settings"];
26
+ export let webSearchMessages: WebSearchMessage[] = [];
27
 
28
  export let loginRequired = false;
29
  $: isReadOnly = !models.some((model) => model.id === currentModel.id);
 
58
  {messages}
59
  readOnly={isReadOnly}
60
  isAuthor={!shared}
61
+ {webSearchMessages}
62
  on:message
63
  on:vote
64
  on:retry={(ev) => {
 
68
  <div
69
  class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
70
  >
71
+ <div class="flex w-full pb-3 max-md:justify-between">
72
+ {#if settings?.searchEnabled}
73
+ <WebSearchToggle />
74
+ {/if}
75
+ {#if loading}
76
+ <StopGeneratingBtn
77
+ classNames={settings?.searchEnabled ? "md:-translate-x-1/2 md:mx-auto" : "mx-auto"}
78
+ on:click={() => dispatch("stop")}
79
+ />
80
+ {/if}
81
  </div>
82
  <form
83
  on:submit|preventDefault={handleSubmit}
 
101
  class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:hidden"
102
  on:click={() => dispatch("stop")}
103
  >
104
+ <CarbonStopFilledAlt />
105
  </button>
106
  <div
107
  class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:flex"
src/lib/components/icons/IconLoading.svelte CHANGED
@@ -2,30 +2,17 @@
2
  export let classNames = "";
3
  </script>
4
 
5
- <svg
6
- xmlns="http://www.w3.org/2000/svg"
7
- width="40px"
8
- height="25px"
9
- viewBox="0 0 60 40"
10
- preserveAspectRatio="xMidYMid"
11
- class={classNames}
12
- >
13
- {#each Array(3) as _, index}
14
- <g transform={`translate(${20 * index + 10} 20)`}>
15
- {index}
16
- <circle cx="0" cy="0" r="6" fill="currentColor">
17
- <animateTransform
18
- attributeName="transform"
19
- type="scale"
20
- begin={`${-0.375 + 0.15 * index}s`}
21
- calcMode="spline"
22
- keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
23
- values="0.5;1;0.5"
24
- keyTimes="0;0.5;1"
25
- dur="1s"
26
- repeatCount="indefinite"
27
- />
28
- </circle>
29
- </g>
30
- {/each}
31
- </svg>
 
2
  export let classNames = "";
3
  </script>
4
 
5
+ <div class={"inline-flex h-8 flex-none items-center gap-1 " + classNames}>
6
+ <div
7
+ class="h-1 w-1 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
8
+ style="animation-delay: 0.25s;"
9
+ />
10
+ <div
11
+ class="h-1 w-1 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
12
+ style="animation-delay: 0.5s;"
13
+ />
14
+ <div
15
+ class="h-1 w-1 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400"
16
+ style="animation-delay: 0.75s;"
17
+ />
18
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/database.ts CHANGED
@@ -2,6 +2,7 @@ import { MONGODB_URL, MONGODB_DB_NAME, MONGODB_DIRECT_CONNECTION } from "$env/st
2
  import { MongoClient } from "mongodb";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
 
5
  import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
6
  import type { Settings } from "$lib/types/Settings";
7
  import type { User } from "$lib/types/User";
@@ -25,6 +26,7 @@ const sharedConversations = db.collection<SharedConversation>("sharedConversatio
25
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
26
  const settings = db.collection<Settings>("settings");
27
  const users = db.collection<User>("users");
 
28
 
29
  export { client, db };
30
  export const collections = {
@@ -33,6 +35,7 @@ export const collections = {
33
  abortedGenerations,
34
  settings,
35
  users,
 
36
  };
37
 
38
  client.on("open", () => {
@@ -48,6 +51,7 @@ client.on("open", () => {
48
  { partialFilterExpression: { userId: { $exists: true } } }
49
  )
50
  .catch(console.error);
 
51
  abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
52
  abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
53
  sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
 
2
  import { MongoClient } from "mongodb";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
+ import type { WebSearch } from "$lib/types/WebSearch";
6
  import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
7
  import type { Settings } from "$lib/types/Settings";
8
  import type { User } from "$lib/types/User";
 
26
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
27
  const settings = db.collection<Settings>("settings");
28
  const users = db.collection<User>("users");
29
+ const webSearches = db.collection<WebSearch>("webSearches");
30
 
31
  export { client, db };
32
  export const collections = {
 
35
  abortedGenerations,
36
  settings,
37
  users,
38
+ webSearches,
39
  };
40
 
41
  client.on("open", () => {
 
51
  { partialFilterExpression: { userId: { $exists: true } } }
52
  )
53
  .catch(console.error);
54
+ webSearches.createIndex({ sessionId: 1, updatedAt: -1 }).catch(console.error);
55
  abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
56
  abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
57
  sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
src/lib/server/generateFromDefaultEndpoint.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defaultModel } from "$lib/server/models";
2
+ import { modelEndpoint } from "./modelEndpoint";
3
+ import { textGeneration } from "@huggingface/inference";
4
+ import { trimSuffix } from "$lib/utils/trimSuffix";
5
+ import { trimPrefix } from "$lib/utils/trimPrefix";
6
+ import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
7
+
8
+ interface Parameters {
9
+ temperature: number;
10
+ truncate: number;
11
+ max_new_tokens: number;
12
+ stop: string[];
13
+ }
14
+ export async function generateFromDefaultEndpoint(
15
+ prompt: string,
16
+ parameters?: Partial<Parameters>
17
+ ) {
18
+ const newParameters = {
19
+ ...defaultModel.parameters,
20
+ ...parameters,
21
+ return_full_text: false,
22
+ };
23
+
24
+ const endpoint = modelEndpoint(defaultModel);
25
+ let { generated_text } = await textGeneration(
26
+ {
27
+ model: endpoint.url,
28
+ inputs: prompt,
29
+ parameters: newParameters,
30
+ },
31
+ {
32
+ fetch: (url, options) =>
33
+ fetch(url, {
34
+ ...options,
35
+ headers: { ...options?.headers, Authorization: endpoint.authorization },
36
+ }),
37
+ }
38
+ );
39
+
40
+ generated_text = trimSuffix(trimPrefix(generated_text, "<|startoftext|>"), PUBLIC_SEP_TOKEN);
41
+
42
+ return generated_text;
43
+ }
src/lib/server/searchWeb.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SERPAPI_KEY } from "$env/static/private";
2
+
3
+ import { getJson } from "serpapi";
4
+ import type { GoogleParameters } from "serpapi";
5
+
6
+ // Show result as JSON
7
+ export async function searchWeb(query: string) {
8
+ const params = {
9
+ q: query,
10
+ hl: "en",
11
+ gl: "us",
12
+ google_domain: "google.com",
13
+ api_key: SERPAPI_KEY,
14
+ } satisfies GoogleParameters;
15
+
16
+ // Show result as JSON
17
+ const response = await getJson("google", params);
18
+
19
+ return response;
20
+ }
src/lib/stores/webSearchParameters.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+ export interface WebSearchParameters {
3
+ useSearch: boolean;
4
+ nItems: number;
5
+ }
6
+ export const webSearchParameters = writable<WebSearchParameters>({
7
+ useSearch: false,
8
+ nItems: 5,
9
+ });
src/lib/types/Message.ts CHANGED
@@ -2,5 +2,6 @@ export interface Message {
2
  from: "user" | "assistant";
3
  id: ReturnType<typeof crypto.randomUUID>;
4
  content: string;
 
5
  score?: -1 | 0 | 1;
6
  }
 
2
  from: "user" | "assistant";
3
  id: ReturnType<typeof crypto.randomUUID>;
4
  content: string;
5
+ webSearchId?: string;
6
  score?: -1 | 0 | 1;
7
  }
src/lib/types/WebSearch.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { Conversation } from "./Conversation";
3
+ import type { Timestamps } from "./Timestamps";
4
+
5
+ export interface WebSearch extends Timestamps {
6
+ _id: ObjectId;
7
+
8
+ convId: Conversation["_id"];
9
+
10
+ prompt: string;
11
+
12
+ searchQuery: string;
13
+ results: string[];
14
+ knowledgeGraph: string;
15
+ summary: string;
16
+
17
+ messages: WebSearchMessage[];
18
+ }
19
+
20
+ export type WebSearchMessageUpdate = {
21
+ type: "update";
22
+ message: string;
23
+ args?: string[];
24
+ };
25
+
26
+ export type WebSearchMessageError = {
27
+ type: "error";
28
+ message: string;
29
+ args?: string[];
30
+ };
31
+
32
+ export type WebSearchMessageResult = {
33
+ type: "result";
34
+ id: string;
35
+ };
36
+
37
+ export type WebSearchMessage =
38
+ | WebSearchMessageUpdate
39
+ | WebSearchMessageResult
40
+ | WebSearchMessageError;
src/routes/+layout.server.ts CHANGED
@@ -6,6 +6,7 @@ import { UrlDependency } from "$lib/types/UrlDependency";
6
  import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
7
  import { authCondition, requiresUser } from "$lib/server/auth";
8
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
 
9
 
10
  export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
11
  const { conversations } = collections;
@@ -60,6 +61,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
60
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
61
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
62
  activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
 
63
  },
64
  models: models.map((model) => ({
65
  id: model.id,
 
6
  import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
7
  import { authCondition, requiresUser } from "$lib/server/auth";
8
  import { DEFAULT_SETTINGS } from "$lib/types/Settings";
9
+ import { SERPAPI_KEY } from "$env/static/private";
10
 
11
  export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
12
  const { conversations } = collections;
 
61
  DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
62
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
63
  activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
64
+ searchEnabled: !!SERPAPI_KEY,
65
  },
66
  models: models.map((model) => ({
67
  id: model.id,
src/routes/conversation/[id]/+page.svelte CHANGED
@@ -12,6 +12,8 @@
12
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
13
  import { randomUUID } from "$lib/utils/randomUuid";
14
  import { findCurrentModel } from "$lib/utils/models";
 
 
15
  import type { Message } from "$lib/types/Message";
16
 
17
  export let data;
@@ -20,6 +22,8 @@
20
  let lastLoadedMessages = data.messages;
21
  let isAborted = false;
22
 
 
 
23
  // Since we modify the messages array locally, we don't want to reset it if an old version is passed
24
  $: if (data.messages !== lastLoadedMessages) {
25
  messages = data.messages;
@@ -29,8 +33,13 @@
29
  let loading = false;
30
  let pending = false;
31
 
32
- async function getTextGenerationStream(inputs: string, messageId: string, isRetry = false) {
33
- const conversationId = $page.params.id;
 
 
 
 
 
34
  const responseId = randomUUID();
35
 
36
  const response = textGenerationStream(
@@ -47,6 +56,7 @@
47
  response_id: responseId,
48
  is_retry: isRetry,
49
  use_cache: false,
 
50
  } as Options
51
  );
52
 
@@ -78,6 +88,7 @@
78
 
79
  if (lastMessage) {
80
  lastMessage.content = output.generated_text;
 
81
  messages = [...messages];
82
  }
83
  break;
@@ -126,7 +137,63 @@
126
  { from: "user", content: message, id: messageId },
127
  ];
128
 
129
- await getTextGenerationStream(message, messageId, isRetry);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  if (messages.filter((m) => m.from === "user").length === 1) {
132
  summarizeTitle($page.params.id)
@@ -186,7 +253,7 @@
186
  writeMessage(val, messageId);
187
  }
188
  });
189
-
190
  $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
191
  </script>
192
 
@@ -198,6 +265,7 @@
198
  {loading}
199
  {pending}
200
  {messages}
 
201
  on:message={(event) => writeMessage(event.detail)}
202
  on:retry={(event) => writeMessage(event.detail.content, event.detail.id)}
203
  on:vote={(event) => voteMessage(event.detail.score, event.detail.id)}
 
12
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
13
  import { randomUUID } from "$lib/utils/randomUuid";
14
  import { findCurrentModel } from "$lib/utils/models";
15
+ import { webSearchParameters } from "$lib/stores/webSearchParameters";
16
+ import type { WebSearchMessage } from "$lib/types/WebSearch.js";
17
  import type { Message } from "$lib/types/Message";
18
 
19
  export let data;
 
22
  let lastLoadedMessages = data.messages;
23
  let isAborted = false;
24
 
25
+ let webSearchMessages: WebSearchMessage[] = [];
26
+
27
  // Since we modify the messages array locally, we don't want to reset it if an old version is passed
28
  $: if (data.messages !== lastLoadedMessages) {
29
  messages = data.messages;
 
33
  let loading = false;
34
  let pending = false;
35
 
36
+ async function getTextGenerationStream(
37
+ inputs: string,
38
+ messageId: string,
39
+ isRetry = false,
40
+ webSearchId?: string
41
+ ) {
42
+ let conversationId = $page.params.id;
43
  const responseId = randomUUID();
44
 
45
  const response = textGenerationStream(
 
56
  response_id: responseId,
57
  is_retry: isRetry,
58
  use_cache: false,
59
+ web_search_id: webSearchId,
60
  } as Options
61
  );
62
 
 
88
 
89
  if (lastMessage) {
90
  lastMessage.content = output.generated_text;
91
+ lastMessage.webSearchId = webSearchId;
92
  messages = [...messages];
93
  }
94
  break;
 
137
  { from: "user", content: message, id: messageId },
138
  ];
139
 
140
+ let searchResponseId: string | null = "";
141
+ if ($webSearchParameters.useSearch) {
142
+ webSearchMessages = [];
143
+
144
+ const res = await fetch(
145
+ `${base}/conversation/${$page.params.id}/web-search?` +
146
+ new URLSearchParams({ prompt: message }),
147
+ {
148
+ method: "GET",
149
+ }
150
+ );
151
+
152
+ // required bc linting doesn't see TextDecoderStream for some reason?
153
+ // eslint-disable-next-line no-undef
154
+ const encoder = new TextDecoderStream();
155
+ const reader = res?.body?.pipeThrough(encoder).getReader();
156
+
157
+ while (searchResponseId === "") {
158
+ await new Promise((r) => setTimeout(r, 25));
159
+
160
+ if (isAborted) {
161
+ reader?.cancel();
162
+ return;
163
+ }
164
+
165
+ reader
166
+ ?.read()
167
+ .then(async ({ done, value }) => {
168
+ if (done) {
169
+ reader.cancel();
170
+ return;
171
+ }
172
+
173
+ try {
174
+ webSearchMessages = (JSON.parse(value) as { messages: WebSearchMessage[] })
175
+ .messages;
176
+ } catch (parseError) {
177
+ // in case of parsing error we wait for the next message
178
+ return;
179
+ }
180
+
181
+ const lastSearchMessage = webSearchMessages[webSearchMessages.length - 1];
182
+ if (lastSearchMessage.type === "result") {
183
+ searchResponseId = lastSearchMessage.id;
184
+ reader.cancel();
185
+ return;
186
+ }
187
+ })
188
+ .catch(() => {
189
+ searchResponseId = null;
190
+ });
191
+ }
192
+ }
193
+
194
+ await getTextGenerationStream(message, messageId, isRetry, searchResponseId ?? undefined);
195
+
196
+ webSearchMessages = [];
197
 
198
  if (messages.filter((m) => m.from === "user").length === 1) {
199
  summarizeTitle($page.params.id)
 
253
  writeMessage(val, messageId);
254
  }
255
  });
256
+ $: $page.params.id, (isAborted = true);
257
  $: title = data.conversations.find((conv) => conv.id === $page.params.id)?.title ?? data.title;
258
  </script>
259
 
 
265
  {loading}
266
  {pending}
267
  {messages}
268
+ bind:webSearchMessages
269
  on:message={(event) => writeMessage(event.detail)}
270
  on:retry={(event) => writeMessage(event.detail.content, event.detail.id)}
271
  on:vote={(event) => voteMessage(event.detail.score, event.detail.id)}
src/routes/conversation/[id]/+server.ts CHANGED
@@ -38,7 +38,7 @@ export async function POST({ request, fetch, locals, params }) {
38
  const json = await request.json();
39
  const {
40
  inputs: newPrompt,
41
- options: { id: messageId, is_retry, response_id: responseId },
42
  } = z
43
  .object({
44
  inputs: z.string().trim().min(1),
@@ -46,6 +46,7 @@ export async function POST({ request, fetch, locals, params }) {
46
  id: z.optional(z.string().uuid()),
47
  response_id: z.optional(z.string().uuid()),
48
  is_retry: z.optional(z.boolean()),
 
49
  }),
50
  })
51
  .parse(json);
@@ -67,8 +68,7 @@ export async function POST({ request, fetch, locals, params }) {
67
  ];
68
  })() satisfies Message[];
69
 
70
- const prompt = buildPrompt(messages, model);
71
-
72
  const randomEndpoint = modelEndpoint(model);
73
 
74
  const abortController = new AbortController();
@@ -114,6 +114,7 @@ export async function POST({ request, fetch, locals, params }) {
114
  messages.push({
115
  from: "assistant",
116
  content: generated_text,
 
117
  id: (responseId as Message["id"]) || crypto.randomUUID(),
118
  });
119
 
@@ -131,7 +132,6 @@ export async function POST({ request, fetch, locals, params }) {
131
  }
132
 
133
  saveMessage().catch(console.error);
134
-
135
  // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors
136
  return new Response(stream1, {
137
  headers: Object.fromEntries(resp.headers.entries()),
 
38
  const json = await request.json();
39
  const {
40
  inputs: newPrompt,
41
+ options: { id: messageId, is_retry, web_search_id, response_id: responseId },
42
  } = z
43
  .object({
44
  inputs: z.string().trim().min(1),
 
46
  id: z.optional(z.string().uuid()),
47
  response_id: z.optional(z.string().uuid()),
48
  is_retry: z.optional(z.boolean()),
49
+ web_search_id: z.ostring(),
50
  }),
51
  })
52
  .parse(json);
 
68
  ];
69
  })() satisfies Message[];
70
 
71
+ const prompt = await buildPrompt(messages, model, web_search_id);
 
72
  const randomEndpoint = modelEndpoint(model);
73
 
74
  const abortController = new AbortController();
 
114
  messages.push({
115
  from: "assistant",
116
  content: generated_text,
117
+ webSearchId: web_search_id,
118
  id: (responseId as Message["id"]) || crypto.randomUUID(),
119
  });
120
 
 
132
  }
133
 
134
  saveMessage().catch(console.error);
 
135
  // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors
136
  return new Response(stream1, {
137
  headers: Object.fromEntries(resp.headers.entries()),
src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts CHANGED
@@ -31,7 +31,7 @@ export async function GET({ params, locals }) {
31
  throw error(404, "Conversation model not found");
32
  }
33
 
34
- const prompt = buildPrompt(conv.messages.slice(0, messageIndex + 1), model);
35
 
36
  return new Response(
37
  JSON.stringify(
 
31
  throw error(404, "Conversation model not found");
32
  }
33
 
34
+ const prompt = await buildPrompt(conv.messages.slice(0, messageIndex + 1), model);
35
 
36
  return new Response(
37
  JSON.stringify(
src/routes/conversation/[id]/summarize/+server.ts CHANGED
@@ -1,16 +1,12 @@
1
  import { buildPrompt } from "$lib/buildPrompt";
2
- import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
3
  import { authCondition } from "$lib/server/auth";
4
  import { collections } from "$lib/server/database";
5
- import { modelEndpoint } from "$lib/server/modelEndpoint";
6
  import { defaultModel } from "$lib/server/models";
7
- import { trimPrefix } from "$lib/utils/trimPrefix";
8
- import { trimSuffix } from "$lib/utils/trimSuffix";
9
- import { textGeneration } from "@huggingface/inference";
10
  import { error } from "@sveltejs/kit";
11
  import { ObjectId } from "mongodb";
12
 
13
- export async function POST({ params, locals, fetch }) {
14
  const convId = new ObjectId(params.id);
15
 
16
  const conversation = await collections.conversations.findOne({
@@ -28,30 +24,8 @@ export async function POST({ params, locals, fetch }) {
28
  `Please summarize the following message as a single sentence of less than 5 words:\n` +
29
  firstMessage?.content;
30
 
31
- const prompt = buildPrompt([{ from: "user", content: userPrompt }], defaultModel);
32
-
33
- const parameters = {
34
- ...defaultModel.parameters,
35
- return_full_text: false,
36
- };
37
-
38
- const endpoint = modelEndpoint(defaultModel);
39
- let { generated_text } = await textGeneration(
40
- {
41
- model: endpoint.url,
42
- inputs: prompt,
43
- parameters,
44
- },
45
- {
46
- fetch: (url, options) =>
47
- fetch(url, {
48
- ...options,
49
- headers: { ...options?.headers, Authorization: endpoint.authorization },
50
- }),
51
- }
52
- );
53
-
54
- generated_text = trimSuffix(trimPrefix(generated_text, "<|startoftext|>"), PUBLIC_SEP_TOKEN);
55
 
56
  if (generated_text) {
57
  await collections.conversations.updateOne(
 
1
  import { buildPrompt } from "$lib/buildPrompt";
 
2
  import { authCondition } from "$lib/server/auth";
3
  import { collections } from "$lib/server/database";
4
+ import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint.js";
5
  import { defaultModel } from "$lib/server/models";
 
 
 
6
  import { error } from "@sveltejs/kit";
7
  import { ObjectId } from "mongodb";
8
 
9
+ export async function POST({ params, locals }) {
10
  const convId = new ObjectId(params.id);
11
 
12
  const conversation = await collections.conversations.findOne({
 
24
  `Please summarize the following message as a single sentence of less than 5 words:\n` +
25
  firstMessage?.content;
26
 
27
+ const prompt = await buildPrompt([{ from: "user", content: userPrompt }], defaultModel);
28
+ const generated_text = await generateFromDefaultEndpoint(prompt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  if (generated_text) {
31
  await collections.conversations.updateOne(
src/routes/conversation/[id]/web-search/+server.ts ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authCondition } from "$lib/server/auth";
2
+ import { collections } from "$lib/server/database";
3
+ import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint.js";
4
+ import { defaultModel } from "$lib/server/models";
5
+ import { searchWeb } from "$lib/server/searchWeb.js";
6
+ import type { Message } from "$lib/types/Message.js";
7
+ import { error } from "@sveltejs/kit";
8
+ import { ObjectId } from "mongodb";
9
+ import { z } from "zod";
10
+ import { JSDOM, VirtualConsole } from "jsdom";
11
+ import type { WebSearch } from "$lib/types/WebSearch.js";
12
+
13
+ function removeTags(node: Node) {
14
+ if (node.hasChildNodes()) {
15
+ node.childNodes.forEach((childNode) => {
16
+ if (node.nodeName === "SCRIPT" || node.nodeName === "STYLE") {
17
+ node.removeChild(childNode);
18
+ } else {
19
+ removeTags(childNode);
20
+ }
21
+ });
22
+ }
23
+ }
24
+ function naiveInnerText(node: Node): string {
25
+ const Node = node; // We need Node(DOM's Node) for the constants, but Node doesn't exist in the nodejs global space, and any Node instance references the constants through the prototype chain
26
+ return [...node.childNodes]
27
+ .map((childNode) => {
28
+ switch (childNode.nodeType) {
29
+ case Node.TEXT_NODE:
30
+ return node.textContent;
31
+ case Node.ELEMENT_NODE:
32
+ return naiveInnerText(childNode);
33
+ default:
34
+ return "";
35
+ }
36
+ })
37
+ .join("\n");
38
+ }
39
+
40
+ interface GenericObject {
41
+ [key: string]: GenericObject | unknown;
42
+ }
43
+
44
+ function removeLinks(obj: GenericObject) {
45
+ for (const prop in obj) {
46
+ if (prop.endsWith("link")) delete obj[prop];
47
+ else if (typeof obj[prop] === "object") removeLinks(obj[prop] as GenericObject);
48
+ }
49
+ return obj;
50
+ }
51
+ export async function GET({ params, locals, url }) {
52
+ const model = defaultModel;
53
+ const convId = new ObjectId(params.id);
54
+ const searchId = new ObjectId();
55
+
56
+ const conv = await collections.conversations.findOne({
57
+ _id: convId,
58
+ ...authCondition(locals),
59
+ });
60
+
61
+ if (!conv) {
62
+ throw error(404, "Conversation not found");
63
+ }
64
+
65
+ const prompt = z.string().trim().min(1).parse(url.searchParams.get("prompt"));
66
+
67
+ const messages = (() => {
68
+ return [...conv.messages, { content: prompt, from: "user", id: crypto.randomUUID() }];
69
+ })() satisfies Message[];
70
+
71
+ const stream = new ReadableStream({
72
+ async start(controller) {
73
+ const webSearch: WebSearch = {
74
+ _id: searchId,
75
+ convId: convId,
76
+ prompt: prompt,
77
+ searchQuery: "",
78
+ knowledgeGraph: "",
79
+ results: [],
80
+ summary: "",
81
+ messages: [],
82
+ createdAt: new Date(),
83
+ updatedAt: new Date(),
84
+ };
85
+ try {
86
+ webSearch.messages.push({
87
+ type: "update",
88
+ message: "Generating search query",
89
+ });
90
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
91
+
92
+ const promptSearchQuery =
93
+ model.userMessageToken +
94
+ "The following messages were written by a user, trying to answer a question." +
95
+ model.messageEndToken +
96
+ messages
97
+ .filter((message) => message.from === "user")
98
+ .map((message) => model.userMessageToken + message.content + model.messageEndToken) +
99
+ model.userMessageToken +
100
+ "What plain-text english sentence would you input into Google to answer the last question? Answer with a short (10 words max) simple sentence." +
101
+ model.messageEndToken +
102
+ model.assistantMessageToken +
103
+ "Query: ";
104
+
105
+ webSearch.searchQuery = await generateFromDefaultEndpoint(promptSearchQuery).then(
106
+ (query) => {
107
+ const arr = query.split(/\r?\n/);
108
+ return arr[0].length > 0 ? arr[0] : arr[1];
109
+ }
110
+ );
111
+ // the model has a tendency to continue answering even when we tell it not to, so the split makes
112
+ // sure we only get the first line of the response
113
+
114
+ webSearch.messages.push({
115
+ type: "update",
116
+ message: "Searching Google",
117
+ args: [webSearch.searchQuery],
118
+ });
119
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
120
+
121
+ const results = await searchWeb(webSearch.searchQuery);
122
+ let text = "";
123
+
124
+ webSearch.results =
125
+ (results.organic_results &&
126
+ results.organic_results.map((el: { link: string }) => el.link)) ??
127
+ [];
128
+
129
+ if (results.knowledge_graph) {
130
+ // if google returns a knowledge graph, we use it
131
+ webSearch.knowledgeGraph = JSON.stringify(removeLinks(results.knowledge_graph));
132
+
133
+ text = webSearch.knowledgeGraph;
134
+
135
+ webSearch.messages.push({
136
+ type: "update",
137
+ message: "Found a Google knowledge page",
138
+ });
139
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
140
+ } else if (webSearch.results.length > 0) {
141
+ // otherwise we use the top result from search
142
+ const topUrl = webSearch.results[0];
143
+
144
+ webSearch.messages.push({
145
+ type: "update",
146
+ message: "Browsing first result",
147
+ args: [JSON.stringify(topUrl)],
148
+ });
149
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
150
+
151
+ // fetch the webpage
152
+ //10 second timeout:
153
+ const abortController = new AbortController();
154
+ setTimeout(() => abortController.abort(), 10000);
155
+ const htmlString = await fetch(topUrl, { signal: abortController.signal })
156
+ .then((response) => response.text())
157
+ .catch((err) => console.log(err));
158
+
159
+ const virtualConsole = new VirtualConsole();
160
+ virtualConsole.on("error", () => {
161
+ // No-op to skip console errors.
162
+ });
163
+
164
+ // put the html string into a DOM
165
+ const dom = new JSDOM(htmlString ?? "", {
166
+ virtualConsole,
167
+ });
168
+
169
+ const body = dom.window.document.querySelector("body");
170
+ if (!body) throw new Error("body of the webpage is null");
171
+
172
+ removeTags(body);
173
+
174
+ // recursively extract text content from the body and then remove newlines and multiple spaces
175
+ text = (naiveInnerText(body) ?? "").replace(/ {2}|\r\n|\n|\r/gm, "");
176
+
177
+ if (!text) throw new Error("text of the webpage is null");
178
+ } else {
179
+ throw new Error("No results found for this search query");
180
+ }
181
+
182
+ webSearch.messages.push({
183
+ type: "update",
184
+ message: "Creating summary",
185
+ });
186
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
187
+
188
+ const summaryPrompt =
189
+ model.userMessageToken +
190
+ text
191
+ .split(" ")
192
+ .slice(0, model.parameters?.truncate ?? 0)
193
+ .join(" ") +
194
+ model.messageEndToken +
195
+ model.userMessageToken +
196
+ `The text above should be summarized to best answer the query: ${webSearch.searchQuery}.` +
197
+ model.messageEndToken +
198
+ model.assistantMessageToken +
199
+ "Summary: ";
200
+
201
+ webSearch.summary = await generateFromDefaultEndpoint(summaryPrompt).then((txt: string) =>
202
+ txt.trim()
203
+ );
204
+
205
+ webSearch.messages.push({
206
+ type: "update",
207
+ message: "Injecting summary",
208
+ args: [JSON.stringify(webSearch.summary)],
209
+ });
210
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
211
+ } catch (searchError) {
212
+ if (searchError instanceof Error) {
213
+ webSearch.messages.push({
214
+ type: "error",
215
+ message: "An error occurred with the web search",
216
+ args: [JSON.stringify(searchError.message)],
217
+ });
218
+ }
219
+ }
220
+
221
+ const res = await collections.webSearches.insertOne(webSearch);
222
+
223
+ webSearch.messages.push({
224
+ type: "result",
225
+ id: res.insertedId.toString(),
226
+ });
227
+ controller.enqueue(JSON.stringify({ messages: webSearch.messages }));
228
+ },
229
+ });
230
+
231
+ return new Response(stream, { headers: { "Content-Type": "application/json" } });
232
+ }
src/routes/r/[id]/message/[messageId]/prompt/+server.ts CHANGED
@@ -26,7 +26,7 @@ export async function GET({ params }) {
26
  throw error(404, "Conversation model not found");
27
  }
28
 
29
- const prompt = buildPrompt(conv.messages.slice(0, messageIndex + 1), model);
30
 
31
  return new Response(
32
  JSON.stringify(
 
26
  throw error(404, "Conversation model not found");
27
  }
28
 
29
+ const prompt = await buildPrompt(conv.messages.slice(0, messageIndex + 1), model);
30
 
31
  return new Response(
32
  JSON.stringify(
src/routes/search/[id]/+server.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { sha256 } from "$lib/utils/sha256.js";
3
+ import { error } from "@sveltejs/kit";
4
+ import { ObjectId } from "mongodb";
5
+
6
+ export async function GET({ params, locals }) {
7
+ const searchId = new ObjectId(params.id);
8
+
9
+ const search = await collections.webSearches.findOne({
10
+ _id: searchId,
11
+ });
12
+
13
+ if (!search) {
14
+ throw error(404, "Search query not found");
15
+ }
16
+
17
+ const conv = await collections.conversations.findOne({
18
+ _id: search.convId,
19
+ });
20
+
21
+ if (!conv) {
22
+ throw error(404, "Conversation not found");
23
+ }
24
+
25
+ // there's no better way to see if a conversation has been shared, so we hash the messages and see if there's a shared conversation with the same hash
26
+ const hash = await sha256(JSON.stringify(conv.messages));
27
+ const sharedConv = await collections.sharedConversations.findOne({
28
+ hash: hash,
29
+ });
30
+
31
+ const userShouldSeeConv =
32
+ (conv.userId && locals.user?._id.toString() === conv.userId.toString()) || sharedConv !== null;
33
+
34
+ if (!userShouldSeeConv) {
35
+ throw error(403, "You don't have access to the conversation here.");
36
+ }
37
+
38
+ return new Response(JSON.stringify(search), { headers: { "Content-Type": "application/json" } });
39
+ }