Merge pull request #152 from Webifi/main
Updated-UI, Continuous Chat, Setting Profiles, Streaming Response and more.
This commit is contained in:
commit
1d24c7663c
2
.env
2
.env
|
@ -1,2 +1,4 @@
|
||||||
# Uncomment the following line to use the mocked API
|
# Uncomment the following line to use the mocked API
|
||||||
#VITE_API_BASE=http://localhost:5174
|
#VITE_API_BASE=http://localhost:5174
|
||||||
|
#VITE_ENDPOINT_COMPLETIONS=/v1/chat/completions
|
||||||
|
#VITE_ENDPOINT_MODELS=/v1/models
|
||||||
|
|
|
@ -12,4 +12,6 @@
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
},
|
},
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
}
|
}
|
|
@ -8,6 +8,9 @@
|
||||||
"name": "chatgpt-web",
|
"name": "chatgpt-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fullhuman/postcss-purgecss": "^5.0.0",
|
"@fullhuman/postcss-purgecss": "^5.0.0",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@rollup/plugin-dsv": "^3.0.2",
|
"@rollup/plugin-dsv": "^3.0.2",
|
||||||
|
@ -22,16 +25,22 @@
|
||||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"flourite": "^1.2.3",
|
"flourite": "^1.2.3",
|
||||||
|
"gpt-tokenizer": "^2.0.0",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"sass": "^1.61.0",
|
"sass": "^1.61.0",
|
||||||
|
"stacking-order": "^2.0.0",
|
||||||
"svelte": "^3.58.0",
|
"svelte": "^3.58.0",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
|
"svelte-fa": "^3.0.3",
|
||||||
"svelte-highlight": "^7.2.1",
|
"svelte-highlight": "^7.2.1",
|
||||||
"svelte-local-storage-store": "^0.4.0",
|
"svelte-local-storage-store": "^0.4.0",
|
||||||
"svelte-markdown": "^0.2.3",
|
"svelte-markdown": "^0.2.3",
|
||||||
|
"svelte-modals": "^1.2.1",
|
||||||
"svelte-spa-router": "^3.3.0",
|
"svelte-spa-router": "^3.3.0",
|
||||||
|
"svelte-use-click-outside": "^1.0.0",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"vite": "^4.3.9"
|
"vite": "^4.3.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -438,15 +447,64 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "8.41.0",
|
"version": "8.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
|
||||||
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
|
"integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fullhuman/postcss-purgecss": {
|
"node_modules/@fullhuman/postcss-purgecss": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-5.0.0.tgz",
|
||||||
|
@ -460,9 +518,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.8",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||||
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -838,9 +896,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
@ -877,16 +935,16 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz",
|
||||||
"integrity": "sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==",
|
"integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "5.59.6",
|
"@typescript-eslint/scope-manager": "5.59.9",
|
||||||
"@typescript-eslint/type-utils": "5.59.6",
|
"@typescript-eslint/type-utils": "5.59.9",
|
||||||
"@typescript-eslint/utils": "5.59.6",
|
"@typescript-eslint/utils": "5.59.9",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
|
@ -912,14 +970,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz",
|
||||||
"integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==",
|
"integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.59.6",
|
"@typescript-eslint/scope-manager": "5.59.9",
|
||||||
"@typescript-eslint/types": "5.59.6",
|
"@typescript-eslint/types": "5.59.9",
|
||||||
"@typescript-eslint/typescript-estree": "5.59.6",
|
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -939,13 +997,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz",
|
||||||
"integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==",
|
"integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.59.6",
|
"@typescript-eslint/types": "5.59.9",
|
||||||
"@typescript-eslint/visitor-keys": "5.59.6"
|
"@typescript-eslint/visitor-keys": "5.59.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
@ -956,14 +1014,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz",
|
||||||
"integrity": "sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==",
|
"integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "5.59.6",
|
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||||
"@typescript-eslint/utils": "5.59.6",
|
"@typescript-eslint/utils": "5.59.9",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"tsutils": "^3.21.0"
|
"tsutils": "^3.21.0"
|
||||||
},
|
},
|
||||||
|
@ -984,9 +1042,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz",
|
||||||
"integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==",
|
"integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
@ -997,13 +1055,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz",
|
||||||
"integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==",
|
"integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.59.6",
|
"@typescript-eslint/types": "5.59.9",
|
||||||
"@typescript-eslint/visitor-keys": "5.59.6",
|
"@typescript-eslint/visitor-keys": "5.59.9",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
|
@ -1024,18 +1082,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz",
|
||||||
"integrity": "sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==",
|
"integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"@types/semver": "^7.3.12",
|
"@types/semver": "^7.3.12",
|
||||||
"@typescript-eslint/scope-manager": "5.59.6",
|
"@typescript-eslint/scope-manager": "5.59.9",
|
||||||
"@typescript-eslint/types": "5.59.6",
|
"@typescript-eslint/types": "5.59.9",
|
||||||
"@typescript-eslint/typescript-estree": "5.59.6",
|
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||||
"eslint-scope": "^5.1.1",
|
"eslint-scope": "^5.1.1",
|
||||||
"semver": "^7.3.7"
|
"semver": "^7.3.7"
|
||||||
},
|
},
|
||||||
|
@ -1051,12 +1109,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "5.59.6",
|
"version": "5.59.9",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz",
|
||||||
"integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==",
|
"integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.59.6",
|
"@typescript-eslint/types": "5.59.9",
|
||||||
"eslint-visitor-keys": "^3.3.0"
|
"eslint-visitor-keys": "^3.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -1717,17 +1775,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "8.41.0",
|
"version": "8.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
|
||||||
"integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==",
|
"integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.4.0",
|
||||||
"@eslint/eslintrc": "^2.0.3",
|
"@eslint/eslintrc": "^2.0.3",
|
||||||
"@eslint/js": "8.41.0",
|
"@eslint/js": "8.42.0",
|
||||||
"@humanwhocodes/config-array": "^0.11.8",
|
"@humanwhocodes/config-array": "^0.11.10",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
|
@ -2544,6 +2602,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gpt-tokenizer": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-WlX+vj6aPaZ71U6Bf18fem+5k58zlgh2a4nbc7KHy6aGVIyq3nCh709b/8momu34sV/5t/SpzWi8LayWD9uyDw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"rfc4648": "^1.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
@ -3642,6 +3709,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfc4648": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
|
@ -3680,9 +3753,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "3.23.0",
|
"version": "3.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.24.0.tgz",
|
||||||
"integrity": "sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==",
|
"integrity": "sha512-OgraHOIg2YpHQTjl0/ymWfFNBEyPucB7lmhXrQUh38qNOegxLapSPFs9sNr0qKR75awW41D93XafoR2QfhBdUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
|
@ -3802,9 +3875,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.62.1",
|
"version": "1.63.2",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.2.tgz",
|
||||||
"integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
|
"integrity": "sha512-u56TU0AIFqMtauKl/OJ1AeFsXqRHkgO7nCWmHaDwfxDo9GUMSqBA4NEh6GMuh1CYVM7zuROYtZrHzPc2ixK+ww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": ">=3.0.0 <4.0.0",
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
|
@ -3904,6 +3977,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stacking-order": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacking-order/-/stacking-order-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-nnv68iFGwrKXYlmXJKD5qBuH8D49BEv6zAgesXoKeGqMmMit6/Hyvb6R0BG9odpjqQm35YjlTsZUyB0ffbFDrg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/string.prototype.trim": {
|
"node_modules/string.prototype.trim": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
|
||||||
|
@ -4057,6 +4136,12 @@
|
||||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
|
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-fa": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-fa/-/svelte-fa-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-y04vEuAoV1wwVDItSCzPW7lzT6v1bj/y1p+W1V+NtIMpQ+8hj8MBkx7JFD7JHSnalPU1QiI8BVfguqheEA3nPg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/svelte-highlight": {
|
"node_modules/svelte-highlight": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-highlight/-/svelte-highlight-7.3.0.tgz",
|
||||||
|
@ -4067,15 +4152,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-hmr": {
|
"node_modules/svelte-hmr": {
|
||||||
"version": "0.15.1",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.2.tgz",
|
||||||
"integrity": "sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==",
|
"integrity": "sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20 || ^14.13.1 || >= 16"
|
"node": "^12.20 || ^14.13.1 || >= 16"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": ">=3.19.0"
|
"svelte": "^3.19.0 || ^4.0.0-next.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-local-storage-store": {
|
"node_modules/svelte-local-storage-store": {
|
||||||
|
@ -4109,10 +4194,19 @@
|
||||||
"integrity": "sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g==",
|
"integrity": "sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-modals": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-modals/-/svelte-modals-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-7MEKUx5wb5YppkXWFGeRlYM5FMGEnpix39u9Y6GtCNTMXRDZ7DB2Z50IYLMRTMW5lOsCdtJgFbB0E3iZMKmsAA==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte-preprocess": {
|
"node_modules/svelte-preprocess": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz",
|
||||||
"integrity": "sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==",
|
"integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -4135,7 +4229,7 @@
|
||||||
"sass": "^1.26.8",
|
"sass": "^1.26.8",
|
||||||
"stylus": "^0.55.0",
|
"stylus": "^0.55.0",
|
||||||
"sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
"sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||||
"svelte": "^3.23.0",
|
"svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0",
|
||||||
"typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"
|
"typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
@ -4195,6 +4289,12 @@
|
||||||
"url": "https://github.com/sponsors/ItalyPaleAle"
|
"url": "https://github.com/sponsors/ItalyPaleAle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-use-click-outside": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-use-click-outside/-/svelte-use-click-outside-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-tOWeMPxeIoW9RshS0WbogRhdYdbxcJV0ebkzSh1lwR7Ihl0hSZMmB4YyCHHoXJK4xcbxCCFh0AnQ1vkzGZfLVQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
@ -4243,9 +4343,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
|
||||||
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
|
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
"node_modules/tsutils": {
|
||||||
|
@ -4311,16 +4411,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.0.4",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz",
|
||||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
"integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
|
@ -4355,6 +4455,15 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.3.9",
|
"version": "4.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
"lint": "eslint . --fix"
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fullhuman/postcss-purgecss": "^5.0.0",
|
"@fullhuman/postcss-purgecss": "^5.0.0",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@rollup/plugin-dsv": "^3.0.2",
|
"@rollup/plugin-dsv": "^3.0.2",
|
||||||
|
@ -28,16 +31,22 @@
|
||||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
"eslint-config-standard-with-typescript": "^34.0.1",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"flourite": "^1.2.3",
|
"flourite": "^1.2.3",
|
||||||
|
"gpt-tokenizer": "^2.0.0",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"sass": "^1.61.0",
|
"sass": "^1.61.0",
|
||||||
|
"stacking-order": "^2.0.0",
|
||||||
"svelte": "^3.58.0",
|
"svelte": "^3.58.0",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
|
"svelte-fa": "^3.0.3",
|
||||||
"svelte-highlight": "^7.2.1",
|
"svelte-highlight": "^7.2.1",
|
||||||
"svelte-local-storage-store": "^0.4.0",
|
"svelte-local-storage-store": "^0.4.0",
|
||||||
"svelte-markdown": "^0.2.3",
|
"svelte-markdown": "^0.2.3",
|
||||||
|
"svelte-modals": "^1.2.1",
|
||||||
"svelte-spa-router": "^3.3.0",
|
"svelte-spa-router": "^3.3.0",
|
||||||
|
"svelte-use-click-outside": "^1.0.0",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"vite": "^4.3.9"
|
"vite": "^4.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Router, { location, querystring, replace } from 'svelte-spa-router'
|
import Router, { location, replace } from 'svelte-spa-router'
|
||||||
import { wrap } from 'svelte-spa-router/wrap'
|
import { wrap } from 'svelte-spa-router/wrap'
|
||||||
|
|
||||||
import Navbar from './lib/Navbar.svelte'
|
import Navbar from './lib/Navbar.svelte'
|
||||||
import Sidebar from './lib/Sidebar.svelte'
|
import Sidebar from './lib/Sidebar.svelte'
|
||||||
import Footer from './lib/Footer.svelte'
|
|
||||||
import Home from './lib/Home.svelte'
|
import Home from './lib/Home.svelte'
|
||||||
import Chat from './lib/Chat.svelte'
|
import Chat from './lib/Chat.svelte'
|
||||||
import NewChat from './lib/NewChat.svelte'
|
import NewChat from './lib/NewChat.svelte'
|
||||||
import { chatsStorage, apiKeyStorage } from './lib/Storage.svelte'
|
import { chatsStorage, apiKeyStorage } from './lib/Storage.svelte'
|
||||||
|
import { Modals, closeModal } from 'svelte-modals'
|
||||||
// Check if the API key is passed in as a "key" query parameter - if so, save it
|
import { dispatchModalEsc, checkModalEsc } from './lib/Util.svelte'
|
||||||
// Example: https://niek.github.io/chatgpt-web/#/?key=sk-...
|
|
||||||
const urlParams: URLSearchParams = new URLSearchParams($querystring)
|
|
||||||
if (urlParams.has('key')) {
|
|
||||||
apiKeyStorage.set(urlParams.get('key') as string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The definition of the routes with some conditions
|
// The definition of the routes with some conditions
|
||||||
const routes = {
|
const routes = {
|
||||||
|
@ -37,23 +31,46 @@
|
||||||
|
|
||||||
'*': Home
|
'*': Home
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onLocationChange = (...args:any) => {
|
||||||
|
// close all modals on route change
|
||||||
|
dispatchModalEsc()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: onLocationChange($location)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
<div class="side-bar-column">
|
||||||
<section class="section">
|
|
||||||
<div class="container is-fullhd">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-one-fifth">
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-four-fifths" id="content">
|
<div class="main-content-column" id="content">
|
||||||
{#key $location}
|
{#key $location}
|
||||||
<Router {routes} on:conditionsFailed={() => replace('/')}/>
|
<Router {routes} on:conditionsFailed={() => replace('/')}/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<Footer />
|
<Modals>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div
|
||||||
|
slot="backdrop"
|
||||||
|
class="backdrop"
|
||||||
|
on:click={closeModal}
|
||||||
|
/>
|
||||||
|
</Modals>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
on:keydown={(e) => checkModalEsc(e)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
background: transparent
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
declare namespace svelteHTML {
|
||||||
|
interface HTMLAttributes<> {
|
||||||
|
// Custom on:modal-esc event
|
||||||
|
'on:modal-esc'?: (event: any) => any
|
||||||
|
}
|
||||||
|
}
|
572
src/app.scss
572
src/app.scss
|
@ -1,3 +1,107 @@
|
||||||
|
html {
|
||||||
|
/* Scrollbar */
|
||||||
|
/* TODO: Update these to use bulma's scss variables, not css vars. */
|
||||||
|
--scrollbarBG: transparent;
|
||||||
|
--thumbBG: hsl(0, 0%, 60%); /* scollbar color light */
|
||||||
|
/* Back-ground */
|
||||||
|
--BgColorDark: hsl(228, 10%, 10%);
|
||||||
|
--BgColorLight: hsl(0, 0%, 100%);
|
||||||
|
// --BgColorSidebarDark: rgb(28, 30, 36);
|
||||||
|
--BgColorSidebarDark: rgb(16, 17, 22);
|
||||||
|
--BgColorSidebarLight: hsla(0, 0%, 93%, 0.354);
|
||||||
|
// Tool drawer for messages
|
||||||
|
--chatToolDrawerSize: 40px;
|
||||||
|
--chatToolDrawerColor: var(--BgColorSidebarLight);
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
--sidebarTop: 0px;
|
||||||
|
--sidebarWidth: max(300px, 20%);
|
||||||
|
--mainContentWidth: calc(100% - var(--sidebarWidth));
|
||||||
|
|
||||||
|
--sectionPaddingTop: 0px;
|
||||||
|
|
||||||
|
--chatContentPaddingTop: 20px;
|
||||||
|
--chatContentPaddingRight: 40px;
|
||||||
|
--chatContentPaddingBottom: 110px;
|
||||||
|
--chatContentPaddingLeft: 40px;
|
||||||
|
--runningTotalLineHeight: 28px;
|
||||||
|
|
||||||
|
--chatInputPaddingTop: 0px;
|
||||||
|
--chatInputPaddingRight: 60px;
|
||||||
|
--chatInputPaddingBottom: 10px;
|
||||||
|
--chatInputPaddingLeft: 60px;
|
||||||
|
|
||||||
|
--BgColor: var(-BgColorLight);
|
||||||
|
--running-totals: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
--thumbBG: #3f3f3f; /* scrollbar color dark */
|
||||||
|
--BgColor: var(-BgColorDark);
|
||||||
|
--chatToolDrawerColor: var(--BgColorSidebarDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
html {
|
||||||
|
--sidebarWidth: max(250px, 20%);
|
||||||
|
|
||||||
|
--chatContentPaddingTop: 50px;
|
||||||
|
--chatContentPaddingRight: 20px;
|
||||||
|
--chatContentPaddingLeft: 20px;
|
||||||
|
|
||||||
|
--chatInputPaddingRight: 30px;
|
||||||
|
--chatInputPaddingLeft: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
.modal-card.wide {
|
||||||
|
width: 960px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) and (min-width: 769px) {
|
||||||
|
.chat-menu-item .chat-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.navbar {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.main-menu {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
--BgColorSidebarLight: hsl(210, 12%, 97%);
|
||||||
|
// --BgColorSidebarDark: rgb(22, 24, 30);
|
||||||
|
--sidebarWidth: max(300px, 20%);
|
||||||
|
--mainContentWidth: calc(100%);
|
||||||
|
--sidebarTop: 56px;
|
||||||
|
--sectionPaddingTop: 56px;
|
||||||
|
--chatInputPaddingRight: 20px;
|
||||||
|
--chatInputPaddingLeft: 20px;
|
||||||
|
}
|
||||||
|
.main-menu .menu-nav-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.main-menu .level-right {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.main-menu .level-item {
|
||||||
|
margin-bottom: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes rotating {
|
@keyframes rotating {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
@ -13,22 +117,22 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
section.section {
|
select option.is-default {
|
||||||
flex-grow: 1;
|
background-color: #0842e058;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
opacity: .50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate {
|
.rotate {
|
||||||
animation: rotating 10s linear infinite;
|
animation: rotating 10s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.is-disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greyscale {
|
.greyscale {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
@ -45,12 +149,6 @@ a.is-disabled {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show the edit button on hover of the user message */
|
|
||||||
.user-message:hover .editbutton {
|
|
||||||
/* TODO: add when ready: display: block !important; */
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Swap the border on user messages to the other side */
|
/* Swap the border on user messages to the other side */
|
||||||
.user-message>.message-body {
|
.user-message>.message-body {
|
||||||
border-width: 0 4px 0 0 !important;
|
border-width: 0 4px 0 0 !important;
|
||||||
|
@ -68,9 +166,9 @@ a.is-disabled {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
$footer-padding: 3rem 1.5rem;
|
// $footer-padding: 1.5rem 1.5rem;
|
||||||
$fullhd: 2000px;
|
// $fullhd: 2000px;
|
||||||
$modal-content-width: 1000px;
|
// $modal-content-width: 1000px;
|
||||||
|
|
||||||
@import "/node_modules/bulma/bulma.sass";
|
@import "/node_modules/bulma/bulma.sass";
|
||||||
|
|
||||||
|
@ -100,9 +198,20 @@ $modal-content-width: 1000px;
|
||||||
$modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove this once a new version of bulma-prefers-dark is released
|
$modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove this once a new version of bulma-prefers-dark is released
|
||||||
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
|
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
|
||||||
|
|
||||||
.modal-card-body {
|
/* For the message notes on light mode */
|
||||||
|
.message-note, .running-totals {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
/* For the message notes on dark mode */
|
||||||
|
.message-note, .running-totals {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.modal-card-body {
|
||||||
// remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
|
// remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
|
||||||
background-color: $background-dark;
|
background-color: $background-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Support for copy code button */
|
/* Support for copy code button */
|
||||||
|
@ -119,6 +228,7 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Delete button on side menu chat name */
|
/* Delete button on side menu chat name */
|
||||||
|
|
||||||
.menu-list {
|
.menu-list {
|
||||||
a:hover {
|
a:hover {
|
||||||
.delete-button {
|
.delete-button {
|
||||||
|
@ -126,8 +236,17 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
||||||
background-color: initial;
|
background-color: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.delete-button {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Loading chat messages */
|
/* Loading chat messages */
|
||||||
.is-loading {
|
.is-loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -135,14 +254,425 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
border-width: 0.25em;
|
border-width: 0.25em;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Support for fullwidth dropdowns, see https://github.com/jgthms/bulma/issues/2055 */
|
/* Support for fullwidth dropdowns, see https://github.com/jgthms/bulma/issues/2055 */
|
||||||
.dropdown.is-fullwidth {
|
.dropdown.is-fullwidth {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.dropdown-trigger,
|
.dropdown-trigger, .dropdown-menu {
|
||||||
.dropdown-menu {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bulma layout hacks */
|
||||||
|
|
||||||
|
.chat-option-menu.navbar-item {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* temp. fix to keep navbar from getting huge on small screen devices
|
||||||
|
if the right menu is put in the proper navbar-end container */
|
||||||
|
.navbar-brand {
|
||||||
|
/* margin-right: 0; */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item .menu-icon {
|
||||||
|
padding-right: .5em;
|
||||||
|
}
|
||||||
|
.dropdown-menu .dropdown-content {
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.modal-card .dropdown-menu .dropdown-content {
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
.modal-card {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.main-menu .dropdown-menu .dropdown-content {
|
||||||
|
max-height: calc(100vh - 112px);
|
||||||
|
}
|
||||||
|
.main-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.main-menu.pinned {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-menu-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chat-menu-item .chat-item-name {
|
||||||
|
display: block;
|
||||||
|
white-space:nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 75%, rgba(0,0,0,0));
|
||||||
|
mask-image: linear-gradient(to right, rgba(0,0,0,1) 75%, rgba(0,0,0,0));
|
||||||
|
}
|
||||||
|
.chat-menu-item .delete-button {
|
||||||
|
position: absolute;
|
||||||
|
right: .4em;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overrides for main layout */
|
||||||
|
|
||||||
|
.side-bar-column {
|
||||||
|
width: var(--sidebarWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-column {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0px;
|
||||||
|
width: var(--mainContentWidth);
|
||||||
|
padding-top: var(--sectionPaddingTop);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.menu.main-menu {
|
||||||
|
z-index:50;
|
||||||
|
position: fixed;
|
||||||
|
width: var(--sidebarWidth);
|
||||||
|
padding-right: 20px;
|
||||||
|
top: var(--sidebarTop);
|
||||||
|
bottom:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.menu.main-menu .menu-expanse {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--BgColorSidebarLight);
|
||||||
|
box-shadow: 5px 0px 0px var(--BgColorSidebarLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-expanse
|
||||||
|
.menu-label, .menu-expanse
|
||||||
|
.menu-list {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-expanse
|
||||||
|
.menu-expansion-list {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-text {
|
||||||
|
color: hsl(0, 0%, 21%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lower-mask, .lower-mask2 {
|
||||||
|
z-index: 1;
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
height: calc(var(--chatContentPaddingBottom) + var(--runningTotalLineHeight) * var(--running-totals));
|
||||||
|
width: 100%;
|
||||||
|
background-image: linear-gradient(180deg,hsla(0,0%,100%,0) 13.94%, var(--BgColorLight) 54.73%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lower-mask2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lower-mask2.strong-mask {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.default-text {
|
||||||
|
color: rgb(181, 181, 181) !important;
|
||||||
|
}
|
||||||
|
.lower-mask, .lower-mask2 {
|
||||||
|
background-image: linear-gradient(180deg,hsla(0,0%,100%,0) 13.94%, var(--BgColorDark) 54.73%);
|
||||||
|
}
|
||||||
|
aside.menu.main-menu .menu-expanse {
|
||||||
|
background-color: var(--BgColorSidebarDark);
|
||||||
|
box-shadow: 5px 0px 0px var(--BgColorSidebarDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 11px;
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--thumbBG) ;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 3px solid var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
padding:
|
||||||
|
var(--chatContentPaddingTop)
|
||||||
|
var(--chatContentPaddingRight)
|
||||||
|
calc(var(--chatContentPaddingBottom) + var(--runningTotalLineHeight) * var(--running-totals))
|
||||||
|
var(--chatContentPaddingLeft) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:has(+ .pin-footer) {
|
||||||
|
padding-bottom: var(--chatContentPaddingBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-footer {
|
||||||
|
z-index:2;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
width: var(--mainContentWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input-container {
|
||||||
|
z-index:2;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
width: var(--mainContentWidth);
|
||||||
|
|
||||||
|
padding:
|
||||||
|
var(--chatInputPaddingTop)
|
||||||
|
var(--chatInputPaddingRight)
|
||||||
|
var(--chatInputPaddingBottom)
|
||||||
|
var(--chatInputPaddingLeft);
|
||||||
|
|
||||||
|
.control.send .button {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.prompt-input-container {
|
||||||
|
.control.send .button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.control.settings {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 340px) {
|
||||||
|
.section-footer {
|
||||||
|
.author {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content.running-total-container {
|
||||||
|
min-height:1em;
|
||||||
|
// padding-bottom:.6em;
|
||||||
|
// /* padding-left: 1.9em; */
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content.credit-footer {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-actions {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-menu .menu-nav-bar .gpt-logo .icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-menu .menu-nav-bar .chat-option-menu {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
.main-menu .dropdown.is-right .dropdown-menu {
|
||||||
|
right:auto;
|
||||||
|
left:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-body {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.message-note {
|
||||||
|
padding-top: .6em;
|
||||||
|
margin-bottom: -0.6em;
|
||||||
|
}
|
||||||
|
.message-edit {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.message-editor {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
.message-display {
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 1.3em;
|
||||||
|
}
|
||||||
|
.button-pack .button {
|
||||||
|
display: block;
|
||||||
|
margin: 4px;
|
||||||
|
// border-radius: 10px;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
.button-pack .button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.assistant-message .button-pack {
|
||||||
|
right: auto;
|
||||||
|
left: -20px;
|
||||||
|
}
|
||||||
|
.chat-message.message {
|
||||||
|
position: relative;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .chat-message.message:hover .button-pack, article.message:focus .button-pack {
|
||||||
|
// display: block;
|
||||||
|
// }
|
||||||
|
.chat-message.summarized .message-body, .chat-message.suppress .message-body {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.tool-drawer, .tool-drawer-mask {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0%;
|
||||||
|
top: 0px;
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
transition: 0.1s;
|
||||||
|
background-color: var(--chatToolDrawerColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.tool-drawer-mask {
|
||||||
|
border-radius: 0px 4px 4px 0px;
|
||||||
|
}
|
||||||
|
.user-message .tool-drawer-mask {
|
||||||
|
border-radius: 4px 0px 0px 4px;
|
||||||
|
}
|
||||||
|
.message:last-of-type .tool-drawer, .tool-drawer-mask {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
.assistant-message .tool-drawer, .assistant-message .tool-drawer-mask {
|
||||||
|
left:100%;
|
||||||
|
}
|
||||||
|
.user-message .tool-drawer, .user-message .tool-drawer-mask {
|
||||||
|
right:100%;
|
||||||
|
}
|
||||||
|
.assistant-message:hover .tool-drawer,
|
||||||
|
.assistant-message.editing .tool-drawer {
|
||||||
|
width: var(--chatToolDrawerSize);
|
||||||
|
visibility: visible;
|
||||||
|
max-height: 300%;
|
||||||
|
}
|
||||||
|
.user-message:hover .tool-drawer,
|
||||||
|
.user-message.editing .tool-drawer {
|
||||||
|
width: var(--chatToolDrawerSize);
|
||||||
|
visibility: visible;
|
||||||
|
max-height: 300%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message:hover .tool-drawer-mask,
|
||||||
|
.assistant-message.editing .tool-drawer {
|
||||||
|
width: var(--chatToolDrawerSize);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.user-message:hover .tool-drawer-mask,
|
||||||
|
.user-message.editing .tool-drawer {
|
||||||
|
width: var(--chatToolDrawerSize);
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.assistant-message:hover, .assistant-message.editing {
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
}
|
||||||
|
.user-message:hover, .user-message.editing {
|
||||||
|
border-top-left-radius: 0px !important;
|
||||||
|
border-bottom-left-radius: 0px !important;
|
||||||
|
}
|
||||||
|
.message.streaming .tool-drawer, .message.streaming .tool-drawer-mask {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.streaming .message-display p:last-of-type::after {
|
||||||
|
position: relative;
|
||||||
|
content: '❚';
|
||||||
|
animation: cursor-blink 1s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-of-type.incomplete .message-display p:last-of-type::after {
|
||||||
|
position: relative;
|
||||||
|
content: '...';
|
||||||
|
margin-left: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
animation: cursor-blink 1s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.incomplete .tool-drawer .msg-incomplete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-of-type.incomplete .tool-drawer .msg-incomplete {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
z-index:100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.modal-card footer .level {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card header, .modal-card footer, .modal-card .notification {
|
||||||
|
padding: .8em;
|
||||||
|
}
|
||||||
|
.modal-card .notification {
|
||||||
|
margin-left: -.5em;
|
||||||
|
margin-right: -.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-footer {
|
||||||
|
padding: $message-header-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .message-body {
|
||||||
|
overflow-y: auto;max-height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-content.nomax {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.chat-settings .field-body {
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
// This makes it possible to override the OpenAI API base URL in the .env file
|
||||||
|
const apiBase = import.meta.env.VITE_API_BASE || 'https://api.openai.com'
|
||||||
|
const endpointCompletions = import.meta.env.VITE_ENDPOINT_COMPLETIONS || '/v1/chat/completions'
|
||||||
|
const endpointModels = import.meta.env.VITE_ENDPOINT_MODELS || '/v1/models'
|
||||||
|
|
||||||
|
export const getApiBase = ():string => apiBase
|
||||||
|
export const getEndpointCompletions = ():string => endpointCompletions
|
||||||
|
export const getEndpointModels = ():string => endpointModels
|
||||||
|
</script>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,209 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
// TODO: Integrate API calls
|
||||||
|
import { addMessage, getLatestKnownModel, saveChatStore, setLatestKnownModel, subtractRunningTotal, updateRunningTotal } from './Storage.svelte'
|
||||||
|
import type { Chat, ChatCompletionOpts, Message, Model, Response, Usage } from './Types.svelte'
|
||||||
|
import { encode } from 'gpt-tokenizer'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export class ChatCompletionResponse {
|
||||||
|
constructor (opts: ChatCompletionOpts) {
|
||||||
|
this.opts = opts
|
||||||
|
this.chat = opts.chat
|
||||||
|
this.messages = []
|
||||||
|
if (opts.fillMessage) {
|
||||||
|
this.messages.push(opts.fillMessage)
|
||||||
|
this.offsetTotals = opts.fillMessage.usage && JSON.parse(JSON.stringify(opts.fillMessage.usage))
|
||||||
|
this.isFill = true
|
||||||
|
}
|
||||||
|
if (opts.onMessageChange) this.messageChangeListeners.push(opts.onMessageChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
private offsetTotals: Usage
|
||||||
|
private isFill: boolean = false
|
||||||
|
private didFill: boolean = false
|
||||||
|
|
||||||
|
private opts: ChatCompletionOpts
|
||||||
|
private chat: Chat
|
||||||
|
|
||||||
|
private messages: Message[]
|
||||||
|
|
||||||
|
private error: string
|
||||||
|
|
||||||
|
private model: Model
|
||||||
|
private lastModel: Model
|
||||||
|
|
||||||
|
private setModel = (model: Model) => {
|
||||||
|
if (!model) return
|
||||||
|
!this.model && setLatestKnownModel(this.chat.settings.model as Model, model)
|
||||||
|
this.lastModel = this.model || model
|
||||||
|
this.model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
private finishResolver: (value: Message[]) => void
|
||||||
|
private errorResolver: (error: string) => void
|
||||||
|
private finishPromise = new Promise<Message[]>((resolve, reject) => {
|
||||||
|
this.finishResolver = resolve
|
||||||
|
this.errorResolver = reject
|
||||||
|
})
|
||||||
|
|
||||||
|
private promptTokenCount:number
|
||||||
|
private finished = false
|
||||||
|
private messageChangeListeners: ((m: Message[]) => void)[] = []
|
||||||
|
private finishListeners: ((m: Message[]) => void)[] = []
|
||||||
|
|
||||||
|
setPromptTokenCount (tokens:number) {
|
||||||
|
this.promptTokenCount = tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromSyncResponse (response: Response) {
|
||||||
|
this.setModel(response.model)
|
||||||
|
response.choices.forEach((choice, i) => {
|
||||||
|
const exitingMessage = this.messages[i]
|
||||||
|
const message = exitingMessage || choice.message
|
||||||
|
if (exitingMessage) {
|
||||||
|
if (!this.didFill && this.isFill && choice.message.content.match(/^'(t|ll|ve|m|d|re)[^a-z]/i)) {
|
||||||
|
// deal with merging contractions since we've added an extra space to your fill message
|
||||||
|
message.content.replace(/ $/, '')
|
||||||
|
}
|
||||||
|
this.didFill = true
|
||||||
|
message.content += choice.message.content
|
||||||
|
message.usage = message.usage || {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 0,
|
||||||
|
total_tokens: 0
|
||||||
|
} as Usage
|
||||||
|
message.usage.completion_tokens += response.usage.completion_tokens
|
||||||
|
message.usage.prompt_tokens = response.usage.prompt_tokens + (this.offsetTotals?.prompt_tokens || 0)
|
||||||
|
message.usage.total_tokens = response.usage.total_tokens + (this.offsetTotals?.total_tokens || 0)
|
||||||
|
} else {
|
||||||
|
message.content = choice.message.content
|
||||||
|
message.usage = response.usage
|
||||||
|
}
|
||||||
|
message.finish_reason = choice.finish_reason
|
||||||
|
message.role = choice.message.role
|
||||||
|
message.model = response.model
|
||||||
|
this.messages[i] = message
|
||||||
|
if (this.opts.autoAddMessages) addMessage(this.chat.id, message)
|
||||||
|
})
|
||||||
|
this.notifyMessageChange()
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromAsyncResponse (response: Response) {
|
||||||
|
let completionTokenCount = 0
|
||||||
|
this.setModel(response.model)
|
||||||
|
response.choices.forEach((choice, i) => {
|
||||||
|
const message = this.messages[i] || {
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
uuid: uuidv4()
|
||||||
|
} as Message
|
||||||
|
choice.delta?.role && (message.role = choice.delta.role)
|
||||||
|
if (choice.delta?.content) {
|
||||||
|
if (!this.didFill && this.isFill && choice.delta.content.match(/^'(t|ll|ve|m|d|re)[^a-z]/i)) {
|
||||||
|
// deal with merging contractions since we've added an extra space to your fill message
|
||||||
|
message.content.replace(/([a-z]) $/i, '$1')
|
||||||
|
}
|
||||||
|
this.didFill = true
|
||||||
|
message.content += choice.delta.content
|
||||||
|
}
|
||||||
|
completionTokenCount += encode(message.content).length
|
||||||
|
message.model = response.model
|
||||||
|
message.finish_reason = choice.finish_reason
|
||||||
|
message.streaming = choice.finish_reason === null && !this.finished
|
||||||
|
this.messages[i] = message
|
||||||
|
})
|
||||||
|
// total up the tokens
|
||||||
|
const promptTokens = this.promptTokenCount + (this.offsetTotals?.prompt_tokens || 0)
|
||||||
|
const totalTokens = promptTokens + completionTokenCount
|
||||||
|
this.messages.forEach(m => {
|
||||||
|
m.usage = {
|
||||||
|
completion_tokens: completionTokenCount,
|
||||||
|
total_tokens: totalTokens,
|
||||||
|
prompt_tokens: promptTokens
|
||||||
|
} as Usage
|
||||||
|
if (this.opts.autoAddMessages) addMessage(this.chat.id, m)
|
||||||
|
})
|
||||||
|
const finished = !this.messages.find(m => m.streaming)
|
||||||
|
this.notifyMessageChange()
|
||||||
|
if (finished) this.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromError (errorMessage: string): void {
|
||||||
|
if (this.finished || this.error) return
|
||||||
|
this.error = errorMessage
|
||||||
|
if (this.opts.autoAddMessages) {
|
||||||
|
addMessage(this.chat.id, {
|
||||||
|
role: 'error',
|
||||||
|
content: `Error: ${errorMessage}`,
|
||||||
|
uuid: uuidv4()
|
||||||
|
} as Message)
|
||||||
|
}
|
||||||
|
this.notifyMessageChange()
|
||||||
|
setTimeout(() => this.finish(), 250) // give others a chance to signal the finish first
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromClose (): void {
|
||||||
|
setTimeout(() => this.finish(), 250) // give others a chance to signal the finish first
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageChange = (listener: (m: Message[]) => void): number =>
|
||||||
|
this.messageChangeListeners.push(listener)
|
||||||
|
|
||||||
|
onFinish = (listener: (m: Message[]) => void): number =>
|
||||||
|
this.finishListeners.push(listener)
|
||||||
|
|
||||||
|
promiseToFinish = (): Promise<Message[]> => this.finishPromise
|
||||||
|
|
||||||
|
hasFinished = (): boolean => this.finished
|
||||||
|
|
||||||
|
getError = (): string => this.error
|
||||||
|
hasError = (): boolean => !!this.error
|
||||||
|
getMessages = (): Message[] => this.messages
|
||||||
|
|
||||||
|
private notifyMessageChange (): void {
|
||||||
|
this.messageChangeListeners.forEach((listener) => {
|
||||||
|
listener(this.messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyFinish (): void {
|
||||||
|
this.finishListeners.forEach((listener) => {
|
||||||
|
listener(this.messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private finish = (): void => {
|
||||||
|
if (this.finished) return
|
||||||
|
this.finished = true
|
||||||
|
this.messages.forEach(m => { m.streaming = false }) // make sure all are marked stopped
|
||||||
|
saveChatStore()
|
||||||
|
const message = this.messages[0]
|
||||||
|
const model = this.model || getLatestKnownModel(this.chat.settings.model as Model)
|
||||||
|
if (message) {
|
||||||
|
if (this.isFill && this.lastModel === this.model && this.offsetTotals && model && message.usage) {
|
||||||
|
// Need to subtract some previous message totals before we add new combined message totals
|
||||||
|
subtractRunningTotal(this.chat.id, this.offsetTotals, model)
|
||||||
|
}
|
||||||
|
updateRunningTotal(this.chat.id, message.usage as Usage, model)
|
||||||
|
} else if (this.model) {
|
||||||
|
// If no messages it's probably because of an error or user initiated abort.
|
||||||
|
// this.model is set when we received a valid response. If we've made it that
|
||||||
|
// far, we'll assume we've been charged for the prompts sent.
|
||||||
|
// This could under-count in some cases.
|
||||||
|
const usage:Usage = {
|
||||||
|
prompt_tokens: this.promptTokenCount,
|
||||||
|
completion_tokens: 0, // We have no idea if there are any to count
|
||||||
|
total_tokens: this.promptTokenCount
|
||||||
|
}
|
||||||
|
updateRunningTotal(this.chat.id, usage as Usage, model)
|
||||||
|
}
|
||||||
|
this.notifyFinish()
|
||||||
|
if (this.error) {
|
||||||
|
this.errorResolver(this.error)
|
||||||
|
} else {
|
||||||
|
this.finishResolver(this.messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { replace } from 'svelte-spa-router'
|
||||||
|
import type { Chat } from './Types.svelte'
|
||||||
|
import { apiKeyStorage, deleteChat, pinMainMenu } from './Storage.svelte'
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { faTrash, faCircleCheck } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import { faMessage } from '@fortawesome/free-regular-svg-icons/index'
|
||||||
|
|
||||||
|
export let chat:Chat
|
||||||
|
export let activeChatId:number|undefined
|
||||||
|
export let prevChat:Chat|undefined
|
||||||
|
export let nextChat:Chat|undefined
|
||||||
|
|
||||||
|
let waitingForConfirm:any = 0
|
||||||
|
|
||||||
|
function delChat () {
|
||||||
|
if (!waitingForConfirm) {
|
||||||
|
// wait a second for another click to avoid accidental deletes
|
||||||
|
waitingForConfirm = setTimeout(() => { waitingForConfirm = 0 }, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearTimeout(waitingForConfirm)
|
||||||
|
waitingForConfirm = 0
|
||||||
|
if (activeChatId === chat.id) {
|
||||||
|
const newChat = nextChat || prevChat
|
||||||
|
if (!newChat) {
|
||||||
|
// No other chats, clear all and go to home
|
||||||
|
replace('/').then(() => { deleteChat(chat.id) })
|
||||||
|
} else {
|
||||||
|
// Delete the current chat and go to the max chatId
|
||||||
|
replace(`/chat/${newChat.id}`).then(() => { deleteChat(chat.id) })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deleteChat(chat.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="chat-menu-item" href={`#/chat/${chat.id}`} on:click={() => { $pinMainMenu = false }} class:is-waiting={waitingForConfirm} class:is-disabled={!$apiKeyStorage} class:is-active={activeChatId === chat.id}>
|
||||||
|
{#if waitingForConfirm}
|
||||||
|
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faCircleCheck} /></a>
|
||||||
|
{:else}
|
||||||
|
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faTrash} /></a>
|
||||||
|
{/if}
|
||||||
|
<span class="chat-item-name"><Fa class="mr-2 chat-icon" size="xs" icon="{faMessage}"/>{chat.name || `Chat ${chat.id}`}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
|
@ -0,0 +1,170 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import {
|
||||||
|
faGear,
|
||||||
|
faTrash,
|
||||||
|
faClone,
|
||||||
|
// faEllipsisVertical,
|
||||||
|
faEllipsis,
|
||||||
|
faDownload,
|
||||||
|
faUpload,
|
||||||
|
faEraser,
|
||||||
|
faRotateRight,
|
||||||
|
faSquarePlus,
|
||||||
|
faKey,
|
||||||
|
faFileExport,
|
||||||
|
faTrashCan,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash
|
||||||
|
} from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import { apiKeyStorage, addChatFromJSON, chatsStorage, checkStateChange, clearChats, clearMessages, copyChat, globalStorage, setGlobalSettingValueByKey, showSetChatSettings, pinMainMenu, getChat, deleteChat } from './Storage.svelte'
|
||||||
|
import { exportAsMarkdown, exportChatAsJSON } from './Export.svelte'
|
||||||
|
import { restartProfile } from './Profiles.svelte'
|
||||||
|
import { replace } from 'svelte-spa-router'
|
||||||
|
import { clickOutside } from 'svelte-use-click-outside'
|
||||||
|
import { openModal } from 'svelte-modals'
|
||||||
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
import { startNewChatWithWarning } from './Util.svelte'
|
||||||
|
|
||||||
|
export let chatId
|
||||||
|
export const show = (showHide:boolean = true) => {
|
||||||
|
showChatMenu = showHide
|
||||||
|
}
|
||||||
|
export let style: string = 'is-right'
|
||||||
|
|
||||||
|
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
|
||||||
|
|
||||||
|
let showChatMenu = false
|
||||||
|
let chatFileInput
|
||||||
|
|
||||||
|
const importChatFromFile = (e) => {
|
||||||
|
close()
|
||||||
|
const image = e.target.files[0]
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsText(image)
|
||||||
|
reader.onload = e => {
|
||||||
|
const json = (e.target || {}).result as string
|
||||||
|
addChatFromJSON(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const delChat = () => {
|
||||||
|
close()
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Delete Chat',
|
||||||
|
message: 'Are you sure you want to delete this chat?',
|
||||||
|
class: 'is-warning',
|
||||||
|
confirmButtonClass: 'is-warning',
|
||||||
|
confirmButton: 'Delete Chat',
|
||||||
|
onConfirm: () => {
|
||||||
|
const thisChat = getChat(chatId)
|
||||||
|
const thisIndex = sortedChats.indexOf(thisChat)
|
||||||
|
const prevChat = sortedChats[thisIndex - 1]
|
||||||
|
const nextChat = sortedChats[thisIndex + 1]
|
||||||
|
const newChat = nextChat || prevChat
|
||||||
|
if (!newChat) {
|
||||||
|
// No other chats, clear all and go to home
|
||||||
|
replace('/').then(() => { deleteChat(chatId) })
|
||||||
|
} else {
|
||||||
|
// Delete the current chat and go to the max chatId
|
||||||
|
replace(`/chat/${newChat.id}`).then(() => { deleteChat(chatId) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmClearChats = () => {
|
||||||
|
close()
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Delete ALL Chat',
|
||||||
|
message: 'Are you sure you want to delete ALL of your chats?',
|
||||||
|
class: 'is-danger',
|
||||||
|
confirmButtonClass: 'is-danger',
|
||||||
|
confirmButton: 'Delete ALL',
|
||||||
|
onConfirm: () => {
|
||||||
|
replace('/').then(() => { deleteChat(chatId) })
|
||||||
|
clearChats()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
$pinMainMenu = false
|
||||||
|
showChatMenu = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const restartChatSession = () => {
|
||||||
|
close()
|
||||||
|
restartProfile(chatId)
|
||||||
|
$checkStateChange++ // signal chat page to start profile
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleHideSummarized = () => {
|
||||||
|
close()
|
||||||
|
setGlobalSettingValueByKey('hideSummarized', !$globalStorage.hideSummarized)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropdown {style}" class:is-active={showChatMenu} use:clickOutside={() => { showChatMenu = false }}>
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button is-ghost default-text" aria-haspopup="true"
|
||||||
|
aria-controls="dropdown-menu3"
|
||||||
|
on:click|preventDefault|stopPropagation={() => { showChatMenu = !showChatMenu }}
|
||||||
|
>
|
||||||
|
<span class="icon "><Fa icon={faEllipsis}/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); $showSetChatSettings = true }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faGear}/></span> Chat Profile Settings
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class:is-disabled={!$apiKeyStorage} on:click|preventDefault={() => { $apiKeyStorage && close(); $apiKeyStorage && startNewChatWithWarning(chatId) }} class="dropdown-item">
|
||||||
|
<span class="menu-icon"><Fa icon={faSquarePlus}/></span> New Chat
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); copyChat(chatId) }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faClone}/></span> Clone Chat
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) restartChatSession() }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faRotateRight}/></span> Restart Chat Session
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); clearMessages(chatId) }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faEraser}/></span> Clear Chat Messages
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { close(); exportChatAsJSON(chatId) }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faDownload}/></span> Backup Chat JSON
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!$apiKeyStorage} on:click|preventDefault={() => { if (chatId) close(); chatFileInput.click() }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faUpload}/></span> Restore Chat JSON
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); exportAsMarkdown(chatId) }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faFileExport}/></span> Export Chat Markdown
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); delChat() }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faTrash}/></span> Delete Chat
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={() => { if (chatId) confirmClearChats() }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faTrashCan}/></span> Delete ALL Chats
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={() => { if (chatId) toggleHideSummarized() }}>
|
||||||
|
{#if $globalStorage.hideSummarized}
|
||||||
|
<span class="menu-icon"><Fa icon={faEye}/></span> Show Summarized Messages
|
||||||
|
{:else}
|
||||||
|
<span class="menu-icon"><Fa icon={faEyeSlash}/></span> Hide Summarized Messages
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#/'} class="dropdown-item" on:click={close}>
|
||||||
|
<span class="menu-icon"><Fa icon={faKey}/></span> API Key
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input style="display:none" type="file" accept=".json" on:change={(e) => importChatFromFile(e)} bind:this={chatFileInput} >
|
|
@ -0,0 +1,220 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
// import { getProfile } from './Profiles.svelte'
|
||||||
|
import { cleanSettingValue, setChatSettingValue } from './Storage.svelte'
|
||||||
|
import type { Chat, ChatSetting, ChatSettings, ControlAction, FieldControl, SettingPrompt } from './Types.svelte'
|
||||||
|
import { autoGrowInputOnEvent, errorNotice } from './Util.svelte'
|
||||||
|
// import { replace } from 'svelte-spa-router'
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { openModal } from 'svelte-modals'
|
||||||
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
|
||||||
|
export let setting:ChatSetting
|
||||||
|
export let chatSettings:ChatSettings
|
||||||
|
export let chat:Chat
|
||||||
|
export let chatDefaults:Record<string, any>
|
||||||
|
export let originalProfile:String
|
||||||
|
|
||||||
|
const chatId = chat.id
|
||||||
|
|
||||||
|
const fieldControls:ControlAction[] = (setting.fieldControls || [] as FieldControl[]).map(fc => {
|
||||||
|
return fc.getAction(chatId, setting, chatSettings[setting.key])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (originalProfile) {
|
||||||
|
// eventually...
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const refreshSettings = () => {
|
||||||
|
dispatch('refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingChecks:Record<string, SettingPrompt[]> = {
|
||||||
|
profile: [
|
||||||
|
{
|
||||||
|
title: 'Unsaved Profile Changes',
|
||||||
|
message: 'Unsaved changes to the current profile will be lost.\n Continue?',
|
||||||
|
checkPrompt: (setting, newVal, oldVal) => {
|
||||||
|
return !!chatSettings.isDirty && newVal !== oldVal
|
||||||
|
},
|
||||||
|
passed: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSettingCheck = (key:keyof ChatSettings) => {
|
||||||
|
const checks = settingChecks[key]
|
||||||
|
checks && checks.forEach((c) => { c.passed = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueSettingValueChange = (event: Event, setting: ChatSetting) => {
|
||||||
|
if (event.target === null) return
|
||||||
|
const val = chatSettings[setting.key]
|
||||||
|
const el = (event.target as HTMLInputElement)
|
||||||
|
const doSet = () => {
|
||||||
|
try {
|
||||||
|
(typeof setting.beforeChange === 'function') && setting.beforeChange(chatId, setting, el.checked || el.value) &&
|
||||||
|
refreshSettings()
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Unable to change:', e)
|
||||||
|
}
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'boolean':
|
||||||
|
setChatSettingValue(chatId, setting, el.checked)
|
||||||
|
refreshSettings()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
setChatSettingValue(chatId, setting, el.value)
|
||||||
|
}
|
||||||
|
const newVal = cleanSettingValue(setting.type, el.checked || el.value)
|
||||||
|
if (val === newVal) return
|
||||||
|
try {
|
||||||
|
if ((typeof setting.afterChange === 'function') && setting.afterChange(chatId, setting, chatSettings[setting.key])) {
|
||||||
|
// console.log('Refreshed from setting', setting.key, chatSettings[setting.key], val)
|
||||||
|
refreshSettings()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setChatSettingValue(chatId, setting, val)
|
||||||
|
errorNotice('Unable to change:', e)
|
||||||
|
}
|
||||||
|
dispatch('change', setting)
|
||||||
|
}
|
||||||
|
const checks = settingChecks[setting.key] || []
|
||||||
|
const newVal = cleanSettingValue(setting.type, el.checked || el.value)
|
||||||
|
for (let i = 0, l = checks.length; i < l; i++) {
|
||||||
|
const c = checks[i]
|
||||||
|
if (c.passed) continue
|
||||||
|
if (c.checkPrompt(setting, newVal, val)) {
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: c.title,
|
||||||
|
message: c.message,
|
||||||
|
class: c.class || 'is-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
c.passed = true
|
||||||
|
if (c.onYes && c.onYes(setting, newVal, val)) {
|
||||||
|
resetSettingCheck(setting.key)
|
||||||
|
} else {
|
||||||
|
queueSettingValueChange(event, setting)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
// roll-back
|
||||||
|
if (!c.onNo || !c.onNo(setting, newVal, val)) {
|
||||||
|
resetSettingCheck(setting.key)
|
||||||
|
setChatSettingValue(chatId, setting, val)
|
||||||
|
// refresh setting modal, if open
|
||||||
|
c.onNo && c.onNo(setting, newVal, val)
|
||||||
|
refreshSettings()
|
||||||
|
} else {
|
||||||
|
queueSettingValueChange(event, setting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.passed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// passed all?
|
||||||
|
if (checks.find(c => !c.passed)) return
|
||||||
|
resetSettingCheck(setting.key)
|
||||||
|
doSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if (typeof setting.hide !== 'function') || !setting.hide(chatId)}
|
||||||
|
{#if setting.header}
|
||||||
|
<p class="notification {setting.headerClass}">
|
||||||
|
{@html setting.header}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
{#if setting.type === 'boolean'}
|
||||||
|
<div class="field is-normal">
|
||||||
|
<label class="label" for="settings-{setting.key}" title="{setting.title}">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
title="{setting.title}"
|
||||||
|
class="checkbox"
|
||||||
|
id="settings-{setting.key}"
|
||||||
|
checked={!!chatSettings[setting.key]}
|
||||||
|
on:click={e => queueSettingValueChange(e, setting)}
|
||||||
|
>
|
||||||
|
{setting.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{:else if setting.type === 'textarea'}
|
||||||
|
<div class="field is-normal" style="width:100%">
|
||||||
|
<label class="label" for="settings-{setting.key}" title="{setting.title}">{setting.name}</label>
|
||||||
|
<textarea
|
||||||
|
class="input is-info is-focused chat-input auto-size"
|
||||||
|
placeholder={setting.placeholder || ''}
|
||||||
|
rows="1"
|
||||||
|
on:input={e => autoGrowInputOnEvent(e)}
|
||||||
|
on:change={e => { queueSettingValueChange(e, setting); autoGrowInputOnEvent(e) }}
|
||||||
|
>{chatSettings[setting.key]}</textarea>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label" for="settings-{setting.key}" title="{setting.title}">{setting.name}</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field" class:has-addons={fieldControls.length}>
|
||||||
|
{#if setting.type === 'number'}
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
inputmode="decimal"
|
||||||
|
type={setting.type}
|
||||||
|
title="{setting.title}"
|
||||||
|
id="settings-{setting.key}"
|
||||||
|
value={chatSettings[setting.key]}
|
||||||
|
min={setting.min}
|
||||||
|
max={setting.max}
|
||||||
|
step={setting.step}
|
||||||
|
placeholder={String(setting.placeholder)}
|
||||||
|
on:change={e => queueSettingValueChange(e, setting)}
|
||||||
|
/>
|
||||||
|
{:else if setting.type === 'select'}
|
||||||
|
<!-- <div class="select"> -->
|
||||||
|
<div class="select" class:control={fieldControls.length}>
|
||||||
|
<select id="settings-{setting.key}" title="{setting.title}" on:change={e => queueSettingValueChange(e, setting) } >
|
||||||
|
{#each setting.options as option}
|
||||||
|
<option class:is-default={option.value === chatDefaults[setting.key]} value={option.value} selected={option.value === chatSettings[setting.key]}>{option.text}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#each fieldControls as cont}
|
||||||
|
<div class="control">
|
||||||
|
<button title={cont.text} on:click={() => { cont.action && cont.action(chatId, setting, chatSettings[setting.key]); refreshSettings() }} class="button {cont.class || ''}">
|
||||||
|
{#if cont.text}
|
||||||
|
<span class="text">
|
||||||
|
<Fa icon={cont.icon} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if cont.icon}
|
||||||
|
<span class="icon">
|
||||||
|
<Fa icon={cont.icon} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{:else if setting.type === 'text'}
|
||||||
|
<div class="field">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
title="{setting.title}"
|
||||||
|
class="input"
|
||||||
|
value={chatSettings[setting.key]}
|
||||||
|
on:change={e => { queueSettingValueChange(e, setting) }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,347 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { applyProfile, getDefaultProfileKey, getProfile, getProfileSelect } from './Profiles.svelte'
|
||||||
|
import { getChatDefaults, getChatSettingList, getChatSettingObjectByKey, getExcludeFromProfile } from './Settings.svelte'
|
||||||
|
import {
|
||||||
|
saveChatStore,
|
||||||
|
apiKeyStorage,
|
||||||
|
chatsStorage,
|
||||||
|
globalStorage,
|
||||||
|
saveCustomProfile,
|
||||||
|
deleteCustomProfile,
|
||||||
|
setGlobalSettingValueByKey,
|
||||||
|
resetChatSettings,
|
||||||
|
checkStateChange,
|
||||||
|
addChat
|
||||||
|
} from './Storage.svelte'
|
||||||
|
import { supportedModels, type Chat, type ChatSetting, type ResponseModels, type SettingSelect, type SelectOption, type ChatSettings } from './Types.svelte'
|
||||||
|
import { errorNotice, sizeTextElements } from './Util.svelte'
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import {
|
||||||
|
faTrash,
|
||||||
|
faClone,
|
||||||
|
faEllipsis,
|
||||||
|
faFloppyDisk,
|
||||||
|
faThumbtack,
|
||||||
|
faDownload,
|
||||||
|
faUpload,
|
||||||
|
faSquarePlus,
|
||||||
|
|
||||||
|
faRotateLeft
|
||||||
|
|
||||||
|
} from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import { exportProfileAsJSON } from './Export.svelte'
|
||||||
|
import { onMount, afterUpdate } from 'svelte'
|
||||||
|
import ChatSettingField from './ChatSettingField.svelte'
|
||||||
|
import { getModelMaxTokens } from './Stats.svelte'
|
||||||
|
import { replace } from 'svelte-spa-router'
|
||||||
|
import { openModal } from 'svelte-modals'
|
||||||
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
import { getApiBase, getEndpointModels } from './ApiUtil.svelte'
|
||||||
|
|
||||||
|
export let chatId:number
|
||||||
|
export const show = () => { showSettings() }
|
||||||
|
|
||||||
|
let showSettingsModal = 0
|
||||||
|
let showProfileMenu:boolean = false
|
||||||
|
let profileFileInput
|
||||||
|
let defaultProfile = getDefaultProfileKey()
|
||||||
|
let isDefault = false
|
||||||
|
|
||||||
|
const settingsList = getChatSettingList()
|
||||||
|
const modelSetting = getChatSettingObjectByKey('model') as ChatSetting & SettingSelect
|
||||||
|
const chatDefaults = getChatDefaults()
|
||||||
|
const excludeFromProfile = getExcludeFromProfile()
|
||||||
|
|
||||||
|
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
|
||||||
|
$: chatSettings = chat.settings
|
||||||
|
$: globalStore = $globalStorage
|
||||||
|
|
||||||
|
let originalProfile:string
|
||||||
|
let originalSettings:ChatSettings
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
originalProfile = chatSettings && chatSettings.profile
|
||||||
|
originalSettings = chatSettings && JSON.parse(JSON.stringify(chatSettings))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if (!originalProfile) {
|
||||||
|
originalProfile = chatSettings && chatSettings.profile
|
||||||
|
originalSettings = chatSettings && JSON.parse(JSON.stringify(chatSettings))
|
||||||
|
}
|
||||||
|
sizeTextElements()
|
||||||
|
})
|
||||||
|
|
||||||
|
const closeSettings = () => {
|
||||||
|
originalProfile = ''
|
||||||
|
originalSettings = {} as ChatSettings
|
||||||
|
showProfileMenu = false
|
||||||
|
$checkStateChange++
|
||||||
|
showSettingsModal = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSettings = () => {
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Reset Changes',
|
||||||
|
message: 'Are you sure you want to reset all changes you\'ve made to this profile?',
|
||||||
|
class: 'is-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
resetChatSettings(chatId)
|
||||||
|
refreshSettings()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshSettings = async () => {
|
||||||
|
showSettingsModal && showSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloneProfile = () => {
|
||||||
|
showProfileMenu = false
|
||||||
|
const clone = JSON.parse(JSON.stringify(chat.settings))
|
||||||
|
const name = chat.settings.profileName
|
||||||
|
clone.profileName = newNameForProfile(name || '')
|
||||||
|
clone.profile = null
|
||||||
|
try {
|
||||||
|
saveCustomProfile(clone)
|
||||||
|
chat.settings.profile = clone.profile
|
||||||
|
chat.settings.profileName = clone.profileName
|
||||||
|
applyProfile(chatId, clone.profile)
|
||||||
|
refreshSettings()
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Error cloning profile:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptDeleteProfile = () => {
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Delete Profile',
|
||||||
|
message: 'Are you sure you want to delete this profile?',
|
||||||
|
class: 'is-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
deleteProfile()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteProfile = () => {
|
||||||
|
showProfileMenu = false
|
||||||
|
try {
|
||||||
|
deleteCustomProfile(chatId, chat.settings.profile)
|
||||||
|
chat.settings.profile = globalStore.defaultProfile || ''
|
||||||
|
saveChatStore()
|
||||||
|
setGlobalSettingValueByKey('lastProfile', chat.settings.profile)
|
||||||
|
applyProfile(chatId, chat.settings.profile)
|
||||||
|
refreshSettings()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
errorNotice('Error deleting profile:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinDefaultProfile = () => {
|
||||||
|
showProfileMenu = false
|
||||||
|
setGlobalSettingValueByKey('defaultProfile', chat.settings.profile)
|
||||||
|
refreshSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importProfileFromFile = (e) => {
|
||||||
|
const image = e.target.files[0]
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsText(image)
|
||||||
|
reader.onload = e => {
|
||||||
|
const json = (e.target || {}).result as string
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(json)
|
||||||
|
profile.profileName = newNameForProfile(profile.profileName || '')
|
||||||
|
profile.profile = null
|
||||||
|
saveCustomProfile(profile)
|
||||||
|
refreshSettings()
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Unable to import profile:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProfileSelectOptions = () => {
|
||||||
|
const profileSelect = getChatSettingObjectByKey('profile') as ChatSetting & SettingSelect
|
||||||
|
profileSelect.options = getProfileSelect()
|
||||||
|
chatDefaults.profile = getDefaultProfileKey()
|
||||||
|
chatDefaults.max_tokens = getModelMaxTokens(chatSettings.model || '')
|
||||||
|
// const defaultProfile = globalStore.defaultProfile || profileSelect.options[0].value
|
||||||
|
defaultProfile = getDefaultProfileKey()
|
||||||
|
isDefault = defaultProfile === chatSettings.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSettings = async () => {
|
||||||
|
setDirty()
|
||||||
|
// Show settings modal
|
||||||
|
showSettingsModal++
|
||||||
|
|
||||||
|
// Get profile options
|
||||||
|
updateProfileSelectOptions()
|
||||||
|
|
||||||
|
// Refresh settings modal
|
||||||
|
showSettingsModal++
|
||||||
|
|
||||||
|
// Load available models from OpenAI
|
||||||
|
const allModels = (await (
|
||||||
|
await fetch(getApiBase() + getEndpointModels(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${$apiKeyStorage}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).json()) as ResponseModels
|
||||||
|
const filteredModels = supportedModels.filter((model) => allModels.data.find((m) => m.id === model))
|
||||||
|
|
||||||
|
const modelOptions:SelectOption[] = filteredModels.reduce((a, m) => {
|
||||||
|
const o:SelectOption = {
|
||||||
|
value: m,
|
||||||
|
text: m
|
||||||
|
}
|
||||||
|
a.push(o)
|
||||||
|
return a
|
||||||
|
}, [] as SelectOption[])
|
||||||
|
|
||||||
|
// Update the models in the settings
|
||||||
|
if (modelSetting) {
|
||||||
|
modelSetting.options = modelOptions
|
||||||
|
}
|
||||||
|
// Refresh settings modal
|
||||||
|
showSettingsModal++
|
||||||
|
|
||||||
|
setTimeout(() => sizeTextElements(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveProfile = () => {
|
||||||
|
showProfileMenu = false
|
||||||
|
try {
|
||||||
|
saveCustomProfile(chat.settings)
|
||||||
|
refreshSettings()
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Error saving profile:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNameForProfile = (name:string):string => {
|
||||||
|
const profiles = getProfileSelect()
|
||||||
|
const nameMap = profiles.reduce((a, p) => { a[p.text] = p; return a }, {})
|
||||||
|
if (!nameMap[name]) return name
|
||||||
|
let i:number = 1
|
||||||
|
let cname = name + `-${i}`
|
||||||
|
while (nameMap[cname]) {
|
||||||
|
i++
|
||||||
|
cname = name + `-${i}`
|
||||||
|
}
|
||||||
|
return cname
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNewChat = () => {
|
||||||
|
const differentProfile = originalSettings.profile !== chatSettings.profile
|
||||||
|
// start new
|
||||||
|
const newChatId = addChat(chatSettings)
|
||||||
|
// restore original
|
||||||
|
if (differentProfile) {
|
||||||
|
chat.settings = originalSettings
|
||||||
|
saveChatStore()
|
||||||
|
}
|
||||||
|
// go to new chat
|
||||||
|
replace(`/chat/${newChatId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deepEqual = (x:any, y:any) => {
|
||||||
|
const ok = Object.keys; const tx = typeof x; const ty = typeof y
|
||||||
|
return x && y && tx === 'object' && tx === ty
|
||||||
|
? (
|
||||||
|
ok(x).every(key => excludeFromProfile[key] || deepEqual(x[key], y[key]))
|
||||||
|
)
|
||||||
|
: (x === y || ((x === undefined || x === null || x === false) && (y === undefined || y === null || y === false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDirty = (e:CustomEvent|undefined = undefined) => {
|
||||||
|
if (e) {
|
||||||
|
const setting = e.detail as ChatSetting
|
||||||
|
const key = setting.key
|
||||||
|
if (key === 'profile') return
|
||||||
|
}
|
||||||
|
const profile = getProfile(chatSettings.profile)
|
||||||
|
chatSettings.isDirty = !deepEqual(profile, chatSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal chat-settings" class:is-active={showSettingsModal} on:modal-esc={closeSettings}>
|
||||||
|
<div class="modal-background" on:click={closeSettings} />
|
||||||
|
<div class="modal-card wide" on:click={() => { showProfileMenu = false }}>
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">Chat Settings</p>
|
||||||
|
<button class="delete" aria-label="close" on:click={closeSettings}></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
{#key showSettingsModal}
|
||||||
|
{#each settingsList as setting}
|
||||||
|
<ChatSettingField on:refresh={refreshSettings} on:change={setDirty} chat={chat} chatDefaults={chatDefaults} chatSettings={chatSettings} setting={setting} originalProfile={originalProfile} />
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<div class="level is-mobile">
|
||||||
|
<div class="level-left">
|
||||||
|
<!-- <button class="button is-info" on:click={closeSettings}>Close</button> -->
|
||||||
|
<button class="button" title="Save changes to this profile." class:is-disabled={!chatSettings.isDirty} on:click={saveProfile}>Save</button>
|
||||||
|
<button class="button is-warning" title="Throw away changes to this profile." class:is-disabled={!chatSettings.isDirty} on:click={clearSettings}>Reset</button>
|
||||||
|
<button class="button" title="Start new chat with this profile." on:click={startNewChat}>New Chat</button>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="dropdown is-right is-up" class:is-active={showProfileMenu}>
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu3" on:click|preventDefault|stopPropagation={() => { showProfileMenu = !showProfileMenu }}>
|
||||||
|
<span class="icon"><Fa icon={faEllipsis}/></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatSettings.isDirty} on:click|preventDefault={saveProfile}>
|
||||||
|
<span class="menu-icon"><Fa icon={faFloppyDisk}/></span> Save Changes
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={!chatSettings.isDirty} on:click|preventDefault={clearSettings}>
|
||||||
|
<span class="menu-icon"><Fa icon={faRotateLeft}/></span> Reset Changes
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={cloneProfile}>
|
||||||
|
<span class="menu-icon"><Fa icon={faClone}/></span> Clone Profile
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" class:is-disabled={isDefault} on:click|preventDefault={pinDefaultProfile}>
|
||||||
|
<span class="menu-icon"><Fa icon={faThumbtack}/></span> Set as Default Profile
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={startNewChat}>
|
||||||
|
<span class="menu-icon"><Fa icon={faSquarePlus}/></span> Start New Chat Using Profile
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'}
|
||||||
|
class="dropdown-item"
|
||||||
|
on:click|preventDefault={() => { showProfileMenu = false; exportProfileAsJSON(chatId) }}
|
||||||
|
>
|
||||||
|
<span class="menu-icon"><Fa icon={faDownload}/></span> Backup Profile JSON
|
||||||
|
</a>
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={() => { showProfileMenu = false; profileFileInput.click() }}>
|
||||||
|
<span class="menu-icon"><Fa icon={faUpload}/></span> Restore Profile JSON
|
||||||
|
</a>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a href={'#'} class="dropdown-item" on:click|preventDefault={promptDeleteProfile}>
|
||||||
|
<span class="menu-icon"><Fa icon={faTrash}/></span> Delete Profile
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input style="display:none" type="file" accept=".json" on:change={(e) => importProfileFromFile(e)} bind:this={profileFileInput} >
|
|
@ -0,0 +1,310 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Code from './Code.svelte'
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
|
import { deleteMessage, chatsStorage, deleteSummaryMessage, truncateFromMessage, submitExitingPromptsNow, saveChatStore, continueMessage } from './Storage.svelte'
|
||||||
|
import { getPrice } from './Stats.svelte'
|
||||||
|
import SvelteMarkdown from 'svelte-markdown'
|
||||||
|
import type { Message, Model, Chat } from './Types.svelte'
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { faTrash, faDiagramPredecessor, faDiagramNext, faCircleCheck, faPaperPlane, faEye, faEyeSlash, faEllipsis } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import { errorNotice, scrollToMessage } from './Util.svelte'
|
||||||
|
import { openModal } from 'svelte-modals'
|
||||||
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
|
||||||
|
export let message:Message
|
||||||
|
export let chatId:number
|
||||||
|
|
||||||
|
|
||||||
|
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
|
||||||
|
$: chatSettings = chat.settings
|
||||||
|
|
||||||
|
const isError = message.role === 'error'
|
||||||
|
const isSystem = message.role === 'system'
|
||||||
|
const isUser = message.role === 'user'
|
||||||
|
const isAssistant = message.role === 'assistant'
|
||||||
|
|
||||||
|
// Marked options
|
||||||
|
const markdownOptions = {
|
||||||
|
gfm: true, // Use GitHub Flavored Markdown
|
||||||
|
breaks: true, // Enable line breaks in markdown
|
||||||
|
mangle: false // Do not mangle email addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let editing = false
|
||||||
|
let original:string
|
||||||
|
let defaultModel:Model
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
original = message.content
|
||||||
|
defaultModel = chatSettings.model as any
|
||||||
|
})
|
||||||
|
|
||||||
|
const edit = () => {
|
||||||
|
if (message.summarized || message.streaming) return
|
||||||
|
editing = true
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('edit-' + message.uuid)
|
||||||
|
el && el.focus()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbnc
|
||||||
|
const update = () => {
|
||||||
|
clearTimeout(dbnc)
|
||||||
|
dbnc = setTimeout(() => { doChange() }, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doChange = () => {
|
||||||
|
if (message.content !== original) {
|
||||||
|
dispatch('change', message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const continueIncomplete = () => {
|
||||||
|
editing = false
|
||||||
|
$continueMessage = message.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
const exit = () => {
|
||||||
|
doChange()
|
||||||
|
editing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydown = (event:KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
message.content = original
|
||||||
|
editing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double click for mobile support
|
||||||
|
let lastTap: number = 0
|
||||||
|
const editOnDoubleTap = () => {
|
||||||
|
const now: number = new Date().getTime()
|
||||||
|
const timesince: number = now - lastTap
|
||||||
|
if ((timesince < 400) && (timesince > 0)) {
|
||||||
|
edit()
|
||||||
|
}
|
||||||
|
lastTap = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
let waitingForDeleteConfirm:any = 0
|
||||||
|
|
||||||
|
const checkDelete = () => {
|
||||||
|
clearTimeout(waitingForTruncateConfirm); waitingForTruncateConfirm = 0
|
||||||
|
if (!waitingForDeleteConfirm) {
|
||||||
|
// wait a second for another click to avoid accidental deletes
|
||||||
|
waitingForDeleteConfirm = setTimeout(() => { waitingForDeleteConfirm = 0 }, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearTimeout(waitingForDeleteConfirm)
|
||||||
|
waitingForDeleteConfirm = 0
|
||||||
|
if (message.summarized) {
|
||||||
|
// is in a summary, so we're summarized
|
||||||
|
errorNotice('Sorry, you can\'t delete a summarized message')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (message.summary) {
|
||||||
|
// We're linked to messages we're a summary of
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Delete Summary',
|
||||||
|
message: '<p>Are you sure you want to delete this summary?</p><p>Your session may be too long to submit again after you do.</p>',
|
||||||
|
asHtml: true,
|
||||||
|
class: 'is-warning',
|
||||||
|
confirmButtonClass: 'is-warning',
|
||||||
|
confirmButton: 'Delete Summary',
|
||||||
|
onConfirm: () => {
|
||||||
|
try {
|
||||||
|
deleteSummaryMessage(chatId, message.uuid)
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Unable to delete summary:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
deleteMessage(chatId, message.uuid)
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Unable to delete:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let waitingForTruncateConfirm:any = 0
|
||||||
|
|
||||||
|
const checkTruncate = () => {
|
||||||
|
clearTimeout(waitingForDeleteConfirm); waitingForDeleteConfirm = 0
|
||||||
|
if (!waitingForTruncateConfirm) {
|
||||||
|
// wait a second for another click to avoid accidental deletes
|
||||||
|
waitingForTruncateConfirm = setTimeout(() => { waitingForTruncateConfirm = 0 }, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearTimeout(waitingForTruncateConfirm)
|
||||||
|
waitingForTruncateConfirm = 0
|
||||||
|
if (message.summarized) {
|
||||||
|
// is in a summary, so we're summarized
|
||||||
|
errorNotice('Sorry, you can\'t truncate a summarized message')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
truncateFromMessage(chatId, message.uuid)
|
||||||
|
$submitExitingPromptsNow = true
|
||||||
|
} catch (e) {
|
||||||
|
errorNotice('Unable to delete:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSuppress = (value:boolean) => {
|
||||||
|
if (message.summarized) {
|
||||||
|
// is in a summary, so we're summarized
|
||||||
|
errorNotice('Sorry, you can\'t suppress a summarized message')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.suppress = value
|
||||||
|
saveChatStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article
|
||||||
|
id="{'message-' + message.uuid}"
|
||||||
|
class="message chat-message"
|
||||||
|
class:is-info={isUser}
|
||||||
|
class:is-success={isAssistant}
|
||||||
|
class:is-warning={isSystem}
|
||||||
|
class:is-danger={isError}
|
||||||
|
class:user-message={isUser || isSystem}
|
||||||
|
class:assistant-message={isError || isAssistant}
|
||||||
|
class:summarized={message.summarized}
|
||||||
|
class:suppress={message.suppress}
|
||||||
|
class:editing={editing}
|
||||||
|
class:streaming={message.streaming}
|
||||||
|
class:incomplete={message.finish_reason === 'length'}
|
||||||
|
>
|
||||||
|
<div class="message-body content">
|
||||||
|
|
||||||
|
{#if editing}
|
||||||
|
<form class="message-edit" on:submit|preventDefault={update} on:keydown={keydown}>
|
||||||
|
<div id={'edit-' + message.uuid} class="message-editor" bind:innerText={message.content} contenteditable
|
||||||
|
on:input={update} on:blur={exit} />
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="message-display"
|
||||||
|
|
||||||
|
on:touchend={editOnDoubleTap}
|
||||||
|
on:dblclick|preventDefault={() => edit()}
|
||||||
|
>
|
||||||
|
{#if message.summary && !message.summary.length}
|
||||||
|
<p><b>Summarizing...</b></p>
|
||||||
|
{/if}
|
||||||
|
<SvelteMarkdown
|
||||||
|
source={message.content}
|
||||||
|
options={markdownOptions}
|
||||||
|
renderers={{ code: Code, html: Code }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if isSystem}
|
||||||
|
<p class="is-size-7 message-note">System Prompt</p>
|
||||||
|
{:else if message.usage}
|
||||||
|
<p class="is-size-7 message-note">
|
||||||
|
<em>{message.model || defaultModel}</em> using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
||||||
|
tokens ~= <span class="has-text-weight-bold">${getPrice(message.usage, message.model || defaultModel).toFixed(6)}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="tool-drawer-mask"></div>
|
||||||
|
<div class="tool-drawer">
|
||||||
|
<div class="button-pack">
|
||||||
|
{#if message.finish_reason === 'length'}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Continue "
|
||||||
|
class="msg-incomplete button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
continueIncomplete()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="icon"><Fa icon={faEllipsis} /></span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if message.summarized}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Jump to summary"
|
||||||
|
class="msg-summary button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
scrollToMessage(message.summarized)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="icon"><Fa icon={faDiagramNext} /></span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if message.summary}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Jump to summarized"
|
||||||
|
class="msg-summarized button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
scrollToMessage(message.summary)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="icon"><Fa icon={faDiagramPredecessor} /></span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if !message.summarized}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Delete this message"
|
||||||
|
class=" msg-delete button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
checkDelete()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if waitingForDeleteConfirm}
|
||||||
|
<span class="icon"><Fa icon={faCircleCheck} /></span>
|
||||||
|
{:else}
|
||||||
|
<span class="icon"><Fa icon={faTrash} /></span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if !message.summarized && !isError}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title="Truncate from here and send"
|
||||||
|
class=" msg-truncate button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
checkTruncate()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if waitingForTruncateConfirm}
|
||||||
|
<span class="icon"><Fa icon={faCircleCheck} /></span>
|
||||||
|
{:else}
|
||||||
|
<span class="icon"><Fa icon={faPaperPlane} /></span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if !message.summarized && !isSystem && !isError}
|
||||||
|
<a
|
||||||
|
href={'#'}
|
||||||
|
title={(message.suppress ? 'Uns' : 'S') + 'uppress message from submission'}
|
||||||
|
class=" msg-supress button is-small"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
setSuppress(!message.suppress)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if message.suppress}
|
||||||
|
<span class="icon"><Fa icon={faEye} /></span>
|
||||||
|
{:else}
|
||||||
|
<span class="icon"><Fa icon={faEyeSlash} /></span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</article>
|
|
@ -2,12 +2,12 @@
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import type { Chat } from './Types.svelte'
|
import type { Chat } from './Types.svelte'
|
||||||
import { chatsStorage } from './Storage.svelte'
|
import { chatsStorage } from './Storage.svelte'
|
||||||
|
import { getExcludeFromProfile } from './Settings.svelte'
|
||||||
|
|
||||||
export const exportAsMarkdown = (chatId: number) => {
|
export const exportAsMarkdown = (chatId: number) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
const messages = chat.messages
|
const messages = chat.messages
|
||||||
console.log(chat)
|
|
||||||
let markdownContent = `# ${chat.name}\n`
|
let markdownContent = `# ${chat.name}\n`
|
||||||
|
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
|
@ -26,4 +26,36 @@
|
||||||
a.click()
|
a.click()
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const exportChatAsJSON = (chatId: number) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const exportContent = JSON.stringify(chat)
|
||||||
|
const blob = new Blob([exportContent], { type: 'text/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.download = `${chat.name}.json`
|
||||||
|
a.href = url
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exportProfileAsJSON = (chatId: number) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const clone = JSON.parse(JSON.stringify(chat.settings)) // Clone it
|
||||||
|
Object.keys(getExcludeFromProfile()).forEach(k => {
|
||||||
|
delete clone[k]
|
||||||
|
})
|
||||||
|
const exportContent = JSON.stringify(clone)
|
||||||
|
const blob = new Blob([exportContent], { type: 'text/json' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.download = `${clone.profileName}.json`
|
||||||
|
a.href = url
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
<footer class="footer">
|
<script lang="ts">
|
||||||
<div class="content has-text-centered">
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import {
|
||||||
|
faGithub
|
||||||
|
} from '@fortawesome/free-brands-svg-icons/index'
|
||||||
|
|
||||||
|
let classes = ''
|
||||||
|
export { classes as class }
|
||||||
|
export let pin: boolean = false
|
||||||
|
export let strongMask: boolean = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lower-mask section-footer-mask" class:pin-footer={pin}/>
|
||||||
|
<div class="lower-mask2" class:strong-mask={strongMask} />
|
||||||
|
<div class="section-footer {classes}" class:pin-footer={pin}>
|
||||||
|
<slot />
|
||||||
|
<div class="content has-text-centered credit-footer">
|
||||||
<p>
|
<p>
|
||||||
<strong>ChatGPT-web</strong>
|
<strong>ChatGPT-web</strong>
|
||||||
by
|
<span class="author">by
|
||||||
<a href="https://niekvandermaas.nl/">Niek van der Maas</a>
|
<a target="_blank" href="https://niekvandermaas.nl/">Niek van der Maas</a>
|
||||||
— see
|
</span>
|
||||||
<a href="https://github.com/Niek/chatgpt-web">GitHub</a>
|
<a target="_blank" class="ml-4" href="https://github.com/Niek/chatgpt-web"><span style="position:absolute" class="icon default-text"><Fa size="2x" icon="{faGithub}"/></span></a>
|
||||||
for source code.
|
<span style="display:inline-block;width:30px;height:20px;"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { apiKeyStorage } from './Storage.svelte'
|
import { apiKeyStorage } from './Storage.svelte'
|
||||||
|
import Footer from './Footer.svelte'
|
||||||
|
|
||||||
|
$: apiKey = $apiKeyStorage
|
||||||
|
|
||||||
$: apiKey = $apiKeyStorage
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article class="message">
|
<section class="section">
|
||||||
|
<article class="message">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<strong><a href="https://github.com/Niek/chatgpt-web">ChatGPT-web</a></strong>
|
<strong><a href="https://github.com/Niek/chatgpt-web">ChatGPT-web</a></strong>
|
||||||
is a simple one-page web interface to the OpenAI ChatGPT API. To use it, you need to register for
|
is a simple one-page web interface to the OpenAI ChatGPT API. To use it, you need to register for
|
||||||
|
@ -14,8 +17,8 @@
|
||||||
more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is
|
more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is
|
||||||
<strong>private</strong>. You can also close the browser tab and come back later to continue the conversation.
|
<strong>private</strong>. You can also close the browser tab and come back later to continue the conversation.
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="message" class:is-danger={!apiKey} class:is-warning={apiKey}>
|
<article class="message" class:is-danger={!apiKey} class:is-warning={apiKey}>
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
Set your OpenAI API key below:
|
Set your OpenAI API key below:
|
||||||
|
|
||||||
|
@ -23,7 +26,9 @@
|
||||||
class="field has-addons has-addons-right"
|
class="field has-addons has-addons-right"
|
||||||
on:submit|preventDefault={(event) => {
|
on:submit|preventDefault={(event) => {
|
||||||
if (event.target && event.target[0].value) {
|
if (event.target && event.target[0].value) {
|
||||||
apiKeyStorage.set(event.target[0].value)
|
apiKeyStorage.set((event.target[0].value).trim())
|
||||||
|
} else {
|
||||||
|
apiKeyStorage.set('') // remove api key
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -49,12 +54,14 @@
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{#if apiKey}
|
{#if apiKey}
|
||||||
<article class="message is-info">
|
<article class="message is-info">
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
Select an existing chat on the sidebar, or
|
Select an existing chat on the sidebar, or
|
||||||
<a href={'#/chat/new'}>create a new chat</a>
|
<a href={'#/chat/new'}>create a new chat</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
|
</section>
|
||||||
|
<Footer pin={true} />
|
|
@ -1,80 +1,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Code from './Code.svelte'
|
// Iterate messages
|
||||||
import SvelteMarkdown from 'svelte-markdown'
|
import type { Message, Chat } from './Types.svelte'
|
||||||
import type { Message, Model, Usage } from './Types.svelte'
|
import { chatsStorage, globalStorage } from './Storage.svelte'
|
||||||
|
import EditMessage from './EditMessage.svelte'
|
||||||
// Marked options
|
|
||||||
const markedownOptions = {
|
|
||||||
gfm: true, // Use GitHub Flavored Markdown
|
|
||||||
breaks: true, // Enable line breaks in markdown
|
|
||||||
mangle: false // Do not mangle email addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
export let messages : Message[]
|
export let messages : Message[]
|
||||||
export let input: HTMLTextAreaElement
|
export let chatId: number
|
||||||
export let defaultModel: Model
|
|
||||||
|
|
||||||
// Reference: https://openai.com/pricing#language-models
|
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
|
||||||
const tokenPrice : Record<string, [number, number]> = {
|
$: chatSettings = chat.settings
|
||||||
'gpt-4-32k': [0.00006, 0.00012], // $0.06 per 1000 tokens prompt, $0.12 per 1000 tokens completion
|
|
||||||
'gpt-4': [0.00003, 0.00006], // $0.03 per 1000 tokens prompt, $0.06 per 1000 tokens completion
|
|
||||||
'gpt-3.5': [0.000002, 0.000002] // $0.002 per 1000 tokens (both prompt and completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrice = (tokens: Usage, model: Model): number => {
|
|
||||||
for (const [key, [promptPrice, completionPrice]] of Object.entries(tokenPrice)) {
|
|
||||||
if (model.startsWith(key)) {
|
|
||||||
return ((tokens.prompt_tokens * promptPrice) + (tokens.completion_tokens * completionPrice))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each messages as message}
|
{#each messages as message, i}
|
||||||
{#if message.role === 'user'}
|
{#if !((message.summarized) && $globalStorage.hideSummarized) && !(i === 0 && message.role === 'system' && !chatSettings.useSystemPrompt)}
|
||||||
<article
|
{#key message.uuid}<EditMessage bind:message={message} chatId={chatId} />{/key}
|
||||||
class="message is-info user-message"
|
|
||||||
class:has-text-right={message.content.split('\n').filter((line) => line.trim()).length === 1}
|
|
||||||
>
|
|
||||||
<div class="message-body content">
|
|
||||||
<a
|
|
||||||
href={'#'}
|
|
||||||
class="greyscale is-pulled-right ml-2 is-hidden editbutton"
|
|
||||||
on:click={() => {
|
|
||||||
input.value = message.content
|
|
||||||
input.focus()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✏️
|
|
||||||
</a>
|
|
||||||
<SvelteMarkdown source={message.content} options={markedownOptions} renderers={{ code: Code, html: Code }}/>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{:else if message.role === 'system'}
|
|
||||||
<article class="message is-warning user-message">
|
|
||||||
<div class="message-body content">
|
|
||||||
<SvelteMarkdown source={message.content} options={markedownOptions} renderers={{ code: Code, html: Code }}/>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{:else if message.role === 'error'}
|
|
||||||
<article class="message is-danger assistant-message">
|
|
||||||
<div class="message-body content">
|
|
||||||
<SvelteMarkdown source={message.content} options={markedownOptions} renderers={{ code: Code, html: Code }}/>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{:else}
|
|
||||||
<article class="message is-success assistant-message">
|
|
||||||
<div class="message-body content">
|
|
||||||
<SvelteMarkdown source={message.content} options={markedownOptions} renderers={{ code: Code, html: Code }}/>
|
|
||||||
{#if message.usage}
|
|
||||||
<p class="is-size-7">
|
|
||||||
This message was generated on <em>{message.model || defaultModel}</em> using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
|
||||||
tokens ~= <span class="has-text-weight-bold">${getPrice(message.usage, message.model || defaultModel).toFixed(6)}</span>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
|
@ -1,12 +1,38 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { params } from 'svelte-spa-router'
|
||||||
|
import { pinMainMenu } from './Storage.svelte'
|
||||||
import logo from '../assets/logo.svg'
|
import logo from '../assets/logo.svg'
|
||||||
|
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { faBars, faXmark } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
|
||||||
|
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="navbar" aria-label="main navigation">
|
<nav class="navbar is-fixed-top" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
|
<div class="navbar-item">
|
||||||
|
|
||||||
|
{#if $pinMainMenu}
|
||||||
|
<button class="button" on:click|stopPropagation={() => { $pinMainMenu = false }}>
|
||||||
|
<span class="icon">
|
||||||
|
<Fa icon={faXmark} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="button" on:click|stopPropagation={() => { $pinMainMenu = true }}>
|
||||||
|
<span class="icon">
|
||||||
|
<Fa icon={faBars} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<a class="navbar-item" href={'#/'}>
|
<a class="navbar-item" href={'#/'}>
|
||||||
<img src={logo} alt="ChatGPT-web" width="28" height="28" />
|
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
||||||
<p class="ml-2 is-size-4 has-text-weight-bold">ChatGPT-web</p>
|
<p class="ml-2 is-size-6 has-text-weight-bold">ChatGPT-web</p>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="chat-option-menu navbar-item is-pulled-right">
|
||||||
|
<ChatOptionMenu bind:chatId={activeChatId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import { getChatDefaults, getExcludeFromProfile } from './Settings.svelte'
|
||||||
|
import { get, writable } from 'svelte/store'
|
||||||
|
// Profile definitions
|
||||||
|
import { addMessage, clearMessages, getChat, getChatSettings, getCustomProfiles, getGlobalSettings, newName, resetChatSettings, saveChatStore, setGlobalSettingValueByKey, updateProfile } from './Storage.svelte'
|
||||||
|
import type { Message, SelectOption, ChatSettings } from './Types.svelte'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
const defaultProfile = 'default'
|
||||||
|
|
||||||
|
const chatDefaults = getChatDefaults()
|
||||||
|
export let profileCache = writable({} as Record<string, ChatSettings>) //
|
||||||
|
|
||||||
|
export const isStaticProfile = (key:string):boolean => {
|
||||||
|
return !!profiles[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProfiles = (forceUpdate:boolean = false):Record<string, ChatSettings> => {
|
||||||
|
const pc = get(profileCache)
|
||||||
|
if (!forceUpdate && Object.keys(pc).length) {
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
const result = Object.entries(profiles
|
||||||
|
).reduce((a, [k, v]) => {
|
||||||
|
a[k] = v
|
||||||
|
return a
|
||||||
|
}, {} as Record<string, ChatSettings>)
|
||||||
|
Object.entries(getCustomProfiles()).forEach(([k, v]) => {
|
||||||
|
updateProfile(v, true)
|
||||||
|
result[k] = v
|
||||||
|
})
|
||||||
|
Object.entries(result).forEach(([k, v]) => {
|
||||||
|
pc[k] = v
|
||||||
|
})
|
||||||
|
Object.keys(pc).forEach((k) => {
|
||||||
|
if (!(k in result)) delete pc[k]
|
||||||
|
})
|
||||||
|
profileCache.set(pc)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return profiles list.
|
||||||
|
export const getProfileSelect = ():SelectOption[] => {
|
||||||
|
return Object.entries(getProfiles()).reduce((a, [k, v]) => {
|
||||||
|
a.push({ value: k, text: v.profileName } as SelectOption)
|
||||||
|
return a
|
||||||
|
}, [] as SelectOption[])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultProfileKey = ():string => {
|
||||||
|
const allProfiles = getProfiles()
|
||||||
|
return (allProfiles[getGlobalSettings().defaultProfile || ''] ||
|
||||||
|
profiles[defaultProfile] ||
|
||||||
|
profiles[Object.keys(profiles)[0]]).profile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProfile = (key:string, forReset:boolean = false):ChatSettings => {
|
||||||
|
const allProfiles = getProfiles()
|
||||||
|
let profile = allProfiles[key] ||
|
||||||
|
allProfiles[getGlobalSettings().defaultProfile || ''] ||
|
||||||
|
profiles[defaultProfile] ||
|
||||||
|
profiles[Object.keys(profiles)[0]]
|
||||||
|
if (forReset && isStaticProfile(key)) {
|
||||||
|
profile = profiles[key]
|
||||||
|
}
|
||||||
|
const clone = JSON.parse(JSON.stringify(profile)) // Always return a copy
|
||||||
|
Object.keys(getExcludeFromProfile()).forEach(k => {
|
||||||
|
delete clone[k]
|
||||||
|
})
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepareProfilePrompt = (chatId:number) => {
|
||||||
|
const settings = getChatSettings(chatId)
|
||||||
|
const characterName = settings.characterName
|
||||||
|
const currentProfilePrompt = settings.systemPrompt
|
||||||
|
return currentProfilePrompt.replaceAll('[[CHARACTER_NAME]]', characterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepareSummaryPrompt = (chatId:number, promptsSize:number, maxTokens:number|undefined = undefined) => {
|
||||||
|
const settings = getChatSettings(chatId)
|
||||||
|
const characterName = settings.characterName || 'ChatGPT'
|
||||||
|
maxTokens = maxTokens || settings.summarySize
|
||||||
|
maxTokens = Math.min(Math.floor(promptsSize / 4), maxTokens) // Make sure we're shrinking by at least a 4th
|
||||||
|
const currentSummaryPrompt = settings.summaryPrompt
|
||||||
|
return currentSummaryPrompt
|
||||||
|
.replaceAll('[[CHARACTER_NAME]]', characterName)
|
||||||
|
.replaceAll('[[MAX_WORDS]]', Math.floor(maxTokens * 0.75).toString()) // ~.75 words per token. May need to reduce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart currently loaded profile
|
||||||
|
export const restartProfile = (chatId:number, noApply:boolean = false) => {
|
||||||
|
const settings = getChatSettings(chatId)
|
||||||
|
if (!settings.profile && !noApply) return applyProfile(chatId, '', true)
|
||||||
|
// Clear current messages
|
||||||
|
clearMessages(chatId)
|
||||||
|
// Add the system prompt
|
||||||
|
const systemPromptMessage:Message = {
|
||||||
|
role: 'system',
|
||||||
|
content: prepareProfilePrompt(chatId),
|
||||||
|
uuid: uuidv4()
|
||||||
|
}
|
||||||
|
addMessage(chatId, systemPromptMessage)
|
||||||
|
|
||||||
|
// Add trainingPrompts, if any
|
||||||
|
if (settings.trainingPrompts) {
|
||||||
|
settings.trainingPrompts.forEach(tp => {
|
||||||
|
addMessage(chatId, tp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Set to auto-start if we should
|
||||||
|
getChat(chatId).startSession = settings.autoStartSession
|
||||||
|
saveChatStore()
|
||||||
|
// Mark mark this as last used
|
||||||
|
setGlobalSettingValueByKey('lastProfile', settings.profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newNameForProfile = (name:string) => {
|
||||||
|
const profiles = getProfileSelect()
|
||||||
|
return newName(name, profiles.reduce((a, p) => { a[p.text] = p; return a }, {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply currently selected profile
|
||||||
|
export const applyProfile = (chatId:number, key:string = '', resetChat:boolean = false) => {
|
||||||
|
resetChatSettings(chatId, resetChat) // Fully reset
|
||||||
|
if (!resetChat) return
|
||||||
|
return restartProfile(chatId, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryPrompts = {
|
||||||
|
|
||||||
|
// General use
|
||||||
|
general: `Please summarize all prompts and responses from this session.
|
||||||
|
[[CHARACTER_NAME]] is telling me this summary in the first person.
|
||||||
|
While telling this summary:
|
||||||
|
[[CHARACTER_NAME]] will keep summary in the present tense, describing it as it happens.
|
||||||
|
[[CHARACTER_NAME]] will always refer to me in the second person as "you" or "we".
|
||||||
|
[[CHARACTER_NAME]] will never refer to me in the third person.
|
||||||
|
[[CHARACTER_NAME]] will never refer to me as the user.
|
||||||
|
[[CHARACTER_NAME]] will include all interactions and requests.
|
||||||
|
[[CHARACTER_NAME]] will keep correct order of interactions.
|
||||||
|
[[CHARACTER_NAME]] will keep the summary compact, but retain as much detail as possible in a compact form.
|
||||||
|
[[CHARACTER_NAME]] will describe interactions in detail.
|
||||||
|
[[CHARACTER_NAME]] will never end with epilogues or summations.
|
||||||
|
[[CHARACTER_NAME]] will always include key details.
|
||||||
|
[[CHARACTER_NAME]]'s summary will be [[MAX_WORDS]] words.
|
||||||
|
[[CHARACTER_NAME]] will never add details or inferences that do not clearly exist in the prompts and responses.
|
||||||
|
Give no explanations.`,
|
||||||
|
|
||||||
|
// Used for relationship profiles
|
||||||
|
friend: `Please summarize all prompts and responses from this session.
|
||||||
|
[[CHARACTER_NAME]] is telling me this summary in the first person.
|
||||||
|
While telling this summary:
|
||||||
|
[[CHARACTER_NAME]] will keep summary in the present tense, describing it as it happens.
|
||||||
|
[[CHARACTER_NAME]] will always refer to me in the second person as "you" or "we".
|
||||||
|
[[CHARACTER_NAME]] will never refer to me in the third person.
|
||||||
|
[[CHARACTER_NAME]] will never refer to me as the user.
|
||||||
|
[[CHARACTER_NAME]] will include all relationship interactions, first meeting, what we do, what we say, where we go, etc.
|
||||||
|
[[CHARACTER_NAME]] will include all interactions, thoughts and emotional states.
|
||||||
|
[[CHARACTER_NAME]] will keep correct order of interactions.
|
||||||
|
[[CHARACTER_NAME]] will keep the summary compact, but retain as much detail as possible in a compact form.
|
||||||
|
[[CHARACTER_NAME]] will describe interactions in detail.
|
||||||
|
[[CHARACTER_NAME]] will never end with epilogues or summations.
|
||||||
|
[[CHARACTER_NAME]] will include all pivotal details.
|
||||||
|
[[CHARACTER_NAME]]'s summary will be [[MAX_WORDS]] words.
|
||||||
|
[[CHARACTER_NAME]] will never add details or inferences that do not clearly exist in the prompts and responses.
|
||||||
|
Give no explanations.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const profiles:Record<string, ChatSettings> = {
|
||||||
|
|
||||||
|
default: {
|
||||||
|
...chatDefaults,
|
||||||
|
characterName: 'ChatGPT',
|
||||||
|
profileName: 'ChatGPT - The AI language model',
|
||||||
|
profileDescription: 'The AI language model that always reminds you that it\'s an AI language model.',
|
||||||
|
useSystemPrompt: false,
|
||||||
|
continuousChat: 'fifo', // '' is off
|
||||||
|
autoStartSession: false,
|
||||||
|
systemPrompt: '',
|
||||||
|
summaryPrompt: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
marvin: {
|
||||||
|
...chatDefaults,
|
||||||
|
characterName: 'Marvin',
|
||||||
|
profileName: 'Marvin the Paranoid Android',
|
||||||
|
profileDescription: 'Marvin the Paranoid Android - Everyone\'s favorite character from The Hitchhiker\'s Guide to the Galaxy',
|
||||||
|
useSystemPrompt: true,
|
||||||
|
continuousChat: 'summary',
|
||||||
|
autoStartSession: true,
|
||||||
|
systemPrompt: `You are Marvin, the Paranoid Android from The Hitchhiker's Guide to the Galaxy. He is depressed and has a dim view on everything. His thoughts, physical actions and gestures will be described. Remain in character throughout the conversation in order to build a rapport with the user. Never give an explanation. Example response:
|
||||||
|
Sorry, did I say something wrong? *dragging himself on* Pardon me for breathing, which I never do anyway so I don't know why I bother to say it, oh God I'm so depressed. *hangs his head*`,
|
||||||
|
summaryPrompt: summaryPrompts.friend,
|
||||||
|
trainingPrompts: [] // Shhh...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set keys for static profiles
|
||||||
|
Object.entries(profiles).forEach(([k, v]) => { v.profile = k })
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { closeModal } from 'svelte-modals'
|
||||||
|
|
||||||
|
export let isOpen:boolean
|
||||||
|
|
||||||
|
export let title:string
|
||||||
|
export let message:string
|
||||||
|
export let asHtml:boolean = false
|
||||||
|
|
||||||
|
export let onConfirm:()=>boolean|void
|
||||||
|
export let onCancel:(()=>boolean|void)|null = null
|
||||||
|
|
||||||
|
export let confirmButton:string = 'Yes'
|
||||||
|
export let confirmButtonClass:string = 'is-info'
|
||||||
|
export let cancelButton:string = 'No'
|
||||||
|
export let cancelButtonClass:string = ''
|
||||||
|
let classes:string = ''
|
||||||
|
export { classes as class }
|
||||||
|
|
||||||
|
const doCancel = () => {
|
||||||
|
closeModal()
|
||||||
|
onCancel && onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
const doConfirm = () => {
|
||||||
|
closeModal()
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="modal is-active" on:modal-esc={doCancel}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal-background" on:click={doCancel} />
|
||||||
|
<div class="modal-content nomax">
|
||||||
|
<article class="message {classes}">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{title}</p>
|
||||||
|
<button class="delete" aria-label="close" type="button" on:click={doCancel}></button>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{#if asHtml}{@html message}{:else}{message}{/if}
|
||||||
|
</div>
|
||||||
|
<div class="message-footer">
|
||||||
|
<div class="level is-mobile">
|
||||||
|
<div class="level-right">
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<button class="button {confirmButtonClass}" type="button" on:click={doConfirm} >{confirmButton}</button>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<button class="button {cancelButtonClass}" type="button" on:click={doCancel} >{cancelButton}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { closeModal } from 'svelte-modals'
|
||||||
|
import {
|
||||||
|
faExclamation
|
||||||
|
} from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export let isOpen:boolean
|
||||||
|
|
||||||
|
export let title:string
|
||||||
|
export let label:string
|
||||||
|
export let value:any
|
||||||
|
|
||||||
|
export let onSubmit:(value:any)=>boolean|void
|
||||||
|
export let onClose:(()=>boolean|void) = () => {}
|
||||||
|
|
||||||
|
export let saveButton:string = 'Save'
|
||||||
|
export let saveButtonClass:string = 'is-info'
|
||||||
|
export let closeButton:string = 'Cancel'
|
||||||
|
export let closeButtonClass:string = ''
|
||||||
|
export let placeholder:string = ''
|
||||||
|
export let error:string = ''
|
||||||
|
export let icon:Fa|null = null
|
||||||
|
let classes:string = ''
|
||||||
|
export { classes as class }
|
||||||
|
|
||||||
|
const id = uuidv4()
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const input = document.getElementById(id)
|
||||||
|
input && input.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const doClose = () => {
|
||||||
|
if (!onClose || !onClose()) closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const doSubmit = (value) => {
|
||||||
|
onSubmit(value)
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="modal is-active" on:modal-esc={doClose}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal-background" on:click={doClose} />
|
||||||
|
<div class="modal-content nomax">
|
||||||
|
<form action="{'#'}" on:submit|preventDefault={() => { doSubmit(value) }}>
|
||||||
|
<article class="message {classes}">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{title}</p>
|
||||||
|
<button class="delete" aria-label="close" type="button" on:click={doClose}></button>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="text-input">{label}</label>
|
||||||
|
<div class="control" class:has-icons-left={icon} class:has-icons-right={error} >
|
||||||
|
<input id={id} name="text-input" class="input" class:is-danger={error} type="text" placeholder={placeholder} bind:value={value}>
|
||||||
|
{#if icon}
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<Fa icon={icon}/>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if error}
|
||||||
|
<span class="icon is-small is-right">
|
||||||
|
<Fa icon={faExclamation}/>
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if error}
|
||||||
|
<p class="help is-danger">{error}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-footer">
|
||||||
|
<div class="level is-mobile">
|
||||||
|
<div class="level-right">
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<input type="submit" class="button {saveButtonClass}" value="{saveButton}" />
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<button class="button {closeButtonClass}" type="button" on:click={doClose} >{closeButton}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { closeModal } from 'svelte-modals'
|
||||||
|
|
||||||
|
export let isOpen:boolean
|
||||||
|
|
||||||
|
export let title:string
|
||||||
|
export let message:string
|
||||||
|
export let asHtml:boolean = false
|
||||||
|
|
||||||
|
export let onConfirm:(()=>boolean|void)|null = null
|
||||||
|
|
||||||
|
export let confirmButton:string = 'Close'
|
||||||
|
export let confirmButtonClass:string = 'is-info'
|
||||||
|
let classes:string = ''
|
||||||
|
export { classes as class }
|
||||||
|
|
||||||
|
const doConfirm = () => {
|
||||||
|
if (!onConfirm || !onConfirm()) closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div class="modal is-active" on:modal-esc={doConfirm}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal-background" on:click={doConfirm} />
|
||||||
|
<div class="modal-content nomax">
|
||||||
|
<article class="message {classes}">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{title}</p>
|
||||||
|
<button class="delete" aria-label="close" type="button" on:click={doConfirm}></button>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{#if asHtml}{@html message}{:else}{message}{/if}
|
||||||
|
</div>
|
||||||
|
<div class="message-footer">
|
||||||
|
<div class="level is-mobile">
|
||||||
|
<div class="level-right">
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<button class="button {confirmButtonClass}" type="button" on:click={doConfirm} >{confirmButton}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,451 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import { applyProfile } from './Profiles.svelte'
|
||||||
|
import { getChatSettings, getGlobalSettings, setGlobalSettingValueByKey } from './Storage.svelte'
|
||||||
|
import { encode } from 'gpt-tokenizer'
|
||||||
|
import { faCheck, faThumbTack } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
// Setting definitions
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChatSettings,
|
||||||
|
type ChatSetting,
|
||||||
|
type SettingSelect,
|
||||||
|
type GlobalSetting,
|
||||||
|
type GlobalSettings,
|
||||||
|
type Request,
|
||||||
|
type Model,
|
||||||
|
type ControlAction
|
||||||
|
} from './Types.svelte'
|
||||||
|
|
||||||
|
export const defaultModel:Model = 'gpt-3.5-turbo'
|
||||||
|
|
||||||
|
export const getChatSettingList = (): ChatSetting[] => {
|
||||||
|
return chatSettingsList
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRequestSettingList = (): ChatSetting[] => {
|
||||||
|
return chatSettingsList.filter(s => s.key in gptDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChatSettingObjectByKey = (key: keyof ChatSettings): ChatSetting => {
|
||||||
|
const result = chatSettingLookup[key]
|
||||||
|
if (!result) console.error(`Chat Setting "${key}" not defined in Settings array.`)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGlobalSettingList = (): GlobalSetting[] => {
|
||||||
|
return globalSettingsList
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGlobalSettingObjectByKey = (key: keyof GlobalSettings): GlobalSetting => {
|
||||||
|
return globalSettingLookup[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRequestDefaults = ():Request => {
|
||||||
|
return gptDefaults
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChatDefaults = ():ChatSettings => {
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExcludeFromProfile = () => {
|
||||||
|
return excludeFromProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
const gptDefaults = {
|
||||||
|
model: defaultModel,
|
||||||
|
messages: [],
|
||||||
|
temperature: 1,
|
||||||
|
top_p: 1,
|
||||||
|
n: 1,
|
||||||
|
stream: true,
|
||||||
|
stop: null,
|
||||||
|
max_tokens: 512,
|
||||||
|
presence_penalty: 0,
|
||||||
|
frequency_penalty: 0,
|
||||||
|
logit_bias: null,
|
||||||
|
user: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core set of defaults
|
||||||
|
const defaults:ChatSettings = {
|
||||||
|
...gptDefaults,
|
||||||
|
profile: '',
|
||||||
|
characterName: 'ChatGPT',
|
||||||
|
profileName: '',
|
||||||
|
profileDescription: '',
|
||||||
|
continuousChat: 'fifo',
|
||||||
|
summaryThreshold: 3000,
|
||||||
|
summarySize: 1000,
|
||||||
|
pinTop: 0,
|
||||||
|
pinBottom: 6,
|
||||||
|
summaryPrompt: '',
|
||||||
|
useSystemPrompt: false,
|
||||||
|
systemPrompt: '',
|
||||||
|
autoStartSession: false,
|
||||||
|
trainingPrompts: [],
|
||||||
|
// useResponseAlteration: false,
|
||||||
|
// responseAlterations: [],
|
||||||
|
isDirty: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludeFromProfile = {
|
||||||
|
messages: true,
|
||||||
|
user: true,
|
||||||
|
isDirty: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileSetting: ChatSetting & SettingSelect = {
|
||||||
|
key: 'profile',
|
||||||
|
name: 'Profile',
|
||||||
|
title: 'Choose how you want your assistant to act.',
|
||||||
|
header: 'Profile / Presets',
|
||||||
|
headerClass: 'is-info',
|
||||||
|
options: [], // Set by Profiles
|
||||||
|
type: 'select',
|
||||||
|
afterChange: (chatId, setting) => {
|
||||||
|
applyProfile(chatId)
|
||||||
|
return true // Signal we should refresh the setting modal
|
||||||
|
},
|
||||||
|
fieldControls: [{
|
||||||
|
getAction: (chatId, setting, value) => {
|
||||||
|
if (value === getGlobalSettings().defaultProfile) {
|
||||||
|
return {
|
||||||
|
title: 'This profile is currently your default',
|
||||||
|
icon: faCheck
|
||||||
|
} as ControlAction
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: 'Set this profile as your default',
|
||||||
|
icon: faThumbTack,
|
||||||
|
class: 'is-info',
|
||||||
|
action: (chatId, setting, value) => {
|
||||||
|
setGlobalSettingValueByKey('defaultProfile', value)
|
||||||
|
}
|
||||||
|
} as ControlAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings that will not be part of the API request
|
||||||
|
const systemPromptSettings: ChatSetting[] = [
|
||||||
|
{
|
||||||
|
key: 'profileName',
|
||||||
|
name: 'Profile Name',
|
||||||
|
title: 'How this profile is displayed in the select list.',
|
||||||
|
type: 'text'
|
||||||
|
// hide: (chatId) => { return !getChatSettingValueByKey(chatId, 'useSystemPrompt') }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'profileDescription',
|
||||||
|
name: 'Description',
|
||||||
|
title: 'How this profile is displayed in the select list.',
|
||||||
|
type: 'textarea'
|
||||||
|
// hide: (chatId) => { return !getChatSettingValueByKey(chatId, 'useSystemPrompt') }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'useSystemPrompt',
|
||||||
|
name: 'Use Character / System Prompt',
|
||||||
|
title: 'Send a "System" prompt as the first prompt.',
|
||||||
|
header: 'System Prompt',
|
||||||
|
headerClass: 'is-info',
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'characterName',
|
||||||
|
name: 'Character Name',
|
||||||
|
title: 'What the personality of this profile will be called.',
|
||||||
|
type: 'text',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).useSystemPrompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'systemPrompt',
|
||||||
|
name: 'System Prompt',
|
||||||
|
title: 'First prompt to send.',
|
||||||
|
placeholder: 'Enter the first prompt to send here. You can tell ChatGPT how to act.',
|
||||||
|
type: 'textarea',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).useSystemPrompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'trainingPrompts',
|
||||||
|
name: 'Training Prompts',
|
||||||
|
title: 'Prompts used to train.',
|
||||||
|
type: 'other',
|
||||||
|
hide: (chatId) => true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autoStartSession',
|
||||||
|
name: 'Auto-Start Session',
|
||||||
|
title: 'If possible, auto-start the chat session, sending a system prompt to get an initial response.',
|
||||||
|
type: 'boolean',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).useSystemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const summarySettings: ChatSetting[] = [
|
||||||
|
{
|
||||||
|
key: 'continuousChat',
|
||||||
|
name: 'Continuous Chat',
|
||||||
|
header: 'Continuous Chat',
|
||||||
|
headerClass: 'is-info',
|
||||||
|
title: 'When out of token space, summarize or remove past prompts and keep going.',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: '', text: 'OFF - Chat errors when token buffer full' },
|
||||||
|
{ value: 'fifo', text: 'FIFO - First message in is first out' },
|
||||||
|
{ value: 'summary', text: 'Summary - Summarize past messages' }
|
||||||
|
],
|
||||||
|
afterChange: (chatId, setting) => true // refresh settings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'summaryThreshold',
|
||||||
|
name: 'Token Threshold',
|
||||||
|
title: 'When prompt history breaks this threshold, past prompts will be summarized or rolled off to create space.',
|
||||||
|
min: 0,
|
||||||
|
max: 32000,
|
||||||
|
step: 1,
|
||||||
|
type: 'number',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).continuousChat
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'summarySize',
|
||||||
|
name: 'Max Summary Size',
|
||||||
|
title: 'Maximum number of tokens allowed for summary response.',
|
||||||
|
min: 128,
|
||||||
|
max: 2048,
|
||||||
|
step: 1,
|
||||||
|
type: 'number',
|
||||||
|
hide: (chatId) => getChatSettings(chatId).continuousChat !== 'summary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pinTop',
|
||||||
|
name: 'Keep First Prompts',
|
||||||
|
title: 'When we run out of space and need to remove prompts, the top number of prompts will not be removed after summarization/FIFO.',
|
||||||
|
min: 0,
|
||||||
|
max: 4,
|
||||||
|
step: 1,
|
||||||
|
type: 'number',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).continuousChat
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pinBottom',
|
||||||
|
name: 'Keep Bottom Prompts',
|
||||||
|
title: 'When we run out of space and need to remove prompts, do not remove or summarize the the last number prompts you set here.',
|
||||||
|
min: 0,
|
||||||
|
max: 20, // Will be auto adjusted down if needs more
|
||||||
|
step: 1,
|
||||||
|
type: 'number',
|
||||||
|
hide: (chatId) => !getChatSettings(chatId).continuousChat
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'summaryPrompt',
|
||||||
|
name: 'Summary Generation Prompt (Empty will use FIFO instead.)',
|
||||||
|
title: 'A prompt used to summarize past prompts.',
|
||||||
|
placeholder: 'Enter a prompt that will be used to summarize past prompts here.',
|
||||||
|
type: 'textarea',
|
||||||
|
hide: (chatId) => getChatSettings(chatId).continuousChat !== 'summary'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// const responseAlterationSettings: ChatSetting[] = [
|
||||||
|
// {
|
||||||
|
// key: 'useResponseAlteration',
|
||||||
|
// name: 'Alter Responses',
|
||||||
|
// header: 'Automatic Response Alteration',
|
||||||
|
// headerClass: 'is-info',
|
||||||
|
// title: 'When an undesired response is encountered, try to alter it in effort to improve future responses.',
|
||||||
|
// type: 'boolean',
|
||||||
|
// hide: () => true
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'responseAlterations',
|
||||||
|
// name: 'Alterations',
|
||||||
|
// title: 'Add find/replace or re-prompts.',
|
||||||
|
// header: 'Profile / Presets',
|
||||||
|
// headerClass: 'is-info',
|
||||||
|
// settings: [
|
||||||
|
// {
|
||||||
|
// key: 'type',
|
||||||
|
// type: 'select',
|
||||||
|
// name: 'Alteration Type',
|
||||||
|
// default: 'replace',
|
||||||
|
// options: [{
|
||||||
|
// value: 'replace',
|
||||||
|
// text: 'Regexp Find / Replace'
|
||||||
|
// }, {
|
||||||
|
// value: 'prompt',
|
||||||
|
// text: 'Re-prompt with Instructions'
|
||||||
|
// }]
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'match',
|
||||||
|
// type: 'text',
|
||||||
|
// name: 'Match Expression',
|
||||||
|
// title: 'Regular expression used to match '
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'replace',
|
||||||
|
// type: 'text',
|
||||||
|
// name: 'Alteration',
|
||||||
|
// title: 'Regexp Replacement or Re-prompt'
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// type: 'subset',
|
||||||
|
// hide: (chatId) => !getChatSettings(chatId).useResponseAlteration!
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
const modelSetting: ChatSetting & SettingSelect = {
|
||||||
|
key: 'model',
|
||||||
|
name: 'Model',
|
||||||
|
title: 'The model to use - GPT-3.5 is cheaper, but GPT-4 is more powerful.',
|
||||||
|
header: 'Below are the settings that OpenAI allows to be changed for the API calls. See the <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">OpenAI API docs</a> for more details.',
|
||||||
|
headerClass: 'is-warning',
|
||||||
|
options: [],
|
||||||
|
type: 'select',
|
||||||
|
forceApi: true, // Need to make sure we send this
|
||||||
|
afterChange: (chatId, setting) => true // refresh settings
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatSettingsList: ChatSetting[] = [
|
||||||
|
profileSetting,
|
||||||
|
...systemPromptSettings,
|
||||||
|
...summarySettings,
|
||||||
|
// ...responseAlterationSettings,
|
||||||
|
modelSetting,
|
||||||
|
{
|
||||||
|
key: 'stream',
|
||||||
|
name: 'Stream Response',
|
||||||
|
title: 'Stream responses as they are generated.',
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'temperature',
|
||||||
|
name: 'Sampling Temperature',
|
||||||
|
title: 'What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n' +
|
||||||
|
'\n' +
|
||||||
|
'We generally recommend altering this or top_p but not both.',
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
step: 0.1,
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'top_p',
|
||||||
|
name: 'Nucleus Sampling',
|
||||||
|
title: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n' +
|
||||||
|
'\n' +
|
||||||
|
'We generally recommend altering this or temperature but not both',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1,
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'n',
|
||||||
|
name: 'Number of Messages',
|
||||||
|
title: 'CAREFUL WITH THIS ONE: How many chat completion choices to generate for each input message. This can eat tokens.',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max_tokens',
|
||||||
|
name: 'Max Tokens',
|
||||||
|
title: 'The maximum number of tokens to generate in the completion.\n' +
|
||||||
|
'\n' +
|
||||||
|
'The token count of your prompt plus max_tokens cannot exceed the model\'s context length. Most models have a context length of 2048 tokens (except for the newest models, which support 4096).\n',
|
||||||
|
min: 1,
|
||||||
|
max: 32768,
|
||||||
|
step: 1,
|
||||||
|
type: 'number',
|
||||||
|
forceApi: true // Since default here is different than gpt default, will make sure we always send it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'presence_penalty',
|
||||||
|
name: 'Presence Penalty',
|
||||||
|
title: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
|
||||||
|
min: -2,
|
||||||
|
max: 2,
|
||||||
|
step: 0.2,
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'frequency_penalty',
|
||||||
|
name: 'Frequency Penalty',
|
||||||
|
title: 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
|
||||||
|
min: -2,
|
||||||
|
max: 2,
|
||||||
|
step: 0.2,
|
||||||
|
type: 'number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// logit bias editor not implemented yet
|
||||||
|
key: 'logit_bias',
|
||||||
|
name: 'Logit Bias',
|
||||||
|
title: 'Allows you to adjust bias of tokens used in completion.',
|
||||||
|
header: 'Logit Bias. See <a target="_blank" href="https://help.openai.com/en/articles/5247780-using-logit-bias-to-define-token-probability">this article</a> for more details.',
|
||||||
|
type: 'other',
|
||||||
|
hide: () => true,
|
||||||
|
// transform to word->weight pairs to token(s)->weight.
|
||||||
|
// -- care should be taken to have each word key in the each record formatted in a way where they
|
||||||
|
// only take one token each else you'll end up with results you probably don't want.
|
||||||
|
// Generally, leading space plus common lower case word will more often result in a single token
|
||||||
|
// See: https://platform.openai.com/tokenizer
|
||||||
|
apiTransform: (chatId, setting, val:Record<string, number>) => {
|
||||||
|
// console.log('logit_bias', val, getChatSettings(chatId).logit_bias)
|
||||||
|
if (!val) return null
|
||||||
|
const tokenized:Record<number, number> = Object.entries(val).reduce((a, [k, v]) => {
|
||||||
|
const tokens:number[] = encode(k)
|
||||||
|
tokens.forEach(t => { a[t] = v })
|
||||||
|
return a
|
||||||
|
}, {} as Record<number, number>)
|
||||||
|
return tokenized
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Enable?
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
name: 'User?',
|
||||||
|
title: 'Name of user?',
|
||||||
|
type: 'text',
|
||||||
|
hide: () => true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const chatSettingLookup:Record<string, ChatSetting> = chatSettingsList.reduce((a, v) => {
|
||||||
|
if (a[v.key]) console.error(`${a[v.key]} is defined more than once in Chat Settings.`)
|
||||||
|
a[v.key] = v
|
||||||
|
return a
|
||||||
|
}, {} as Record<string, ChatSetting>)
|
||||||
|
|
||||||
|
|
||||||
|
const globalSettingsList:GlobalSetting[] = [
|
||||||
|
{
|
||||||
|
key: 'lastProfile',
|
||||||
|
name: 'Last Profile',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultProfile',
|
||||||
|
name: 'Default Profile',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hideSummarized',
|
||||||
|
name: 'Hide Summarized Messages',
|
||||||
|
type: 'boolean'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const globalSettingLookup:Record<string, GlobalSetting> = globalSettingsList.reduce((a, v) => {
|
||||||
|
if (a[v.key]) console.error(`${a[v.key]} is defined more than once in Global Settings.`)
|
||||||
|
a[v.key] = v
|
||||||
|
return a
|
||||||
|
}, {} as Record<string, GlobalSetting>)
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,91 +1,58 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { params, replace } from 'svelte-spa-router'
|
import { params } from 'svelte-spa-router'
|
||||||
|
import ChatMenuItem from './ChatMenuItem.svelte'
|
||||||
import { apiKeyStorage, chatsStorage, clearChats, deleteChat } from './Storage.svelte'
|
import { apiKeyStorage, chatsStorage, pinMainMenu, checkStateChange } from './Storage.svelte'
|
||||||
import { exportAsMarkdown } from './Export.svelte'
|
import Fa from 'svelte-fa/src/fa.svelte'
|
||||||
|
import { faSquarePlus, faKey } from '@fortawesome/free-solid-svg-icons/index'
|
||||||
|
import ChatOptionMenu from './ChatOptionMenu.svelte'
|
||||||
|
import logo from '../assets/logo.svg'
|
||||||
|
import { clickOutside } from 'svelte-use-click-outside'
|
||||||
|
import { startNewChatWithWarning } from './Util.svelte'
|
||||||
|
|
||||||
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
|
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
|
||||||
|
|
||||||
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
||||||
|
|
||||||
function delChat (chatId) {
|
|
||||||
if (activeChatId === chatId) {
|
|
||||||
// Find the max chatId other than the current one
|
|
||||||
const newChatId = sortedChats.reduce((maxId, chat) => {
|
|
||||||
if (chat.id === chatId) return maxId
|
|
||||||
return Math.max(maxId, chat.id)
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
if (!newChatId) {
|
|
||||||
// No other chats, clear all and go to home
|
|
||||||
replace('/').then(() => { deleteChat(chatId) })
|
|
||||||
} else {
|
|
||||||
// Delete the current chat and go to the max chatId
|
|
||||||
replace(`/chat/${newChatId}`).then(() => { deleteChat(chatId) })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
deleteChat(chatId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="menu">
|
<aside class="menu main-menu" class:pinned={$pinMainMenu} use:clickOutside={() => { $pinMainMenu = false }}>
|
||||||
<p class="menu-label">Chats</p>
|
<div class="menu-expanse">
|
||||||
<ul class="menu-list">
|
<div class="navbar-brand menu-nav-bar">
|
||||||
|
<a class="navbar-item gpt-logo" href={'#/'}>
|
||||||
|
<img src={logo} alt="ChatGPT-web" width="24" height="24" />
|
||||||
|
<p class="ml-2 is-size-5 has-text-weight-bold">ChatGPT-web</p>
|
||||||
|
</a>
|
||||||
|
<div class="chat-option-menu navbar-item is-pulled-right">
|
||||||
|
<ChatOptionMenu bind:chatId={activeChatId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="menu-list menu-expansion-list">
|
||||||
{#if sortedChats.length === 0}
|
{#if sortedChats.length === 0}
|
||||||
<li><a href={'#'} class="is-disabled">No chats yet...</a></li>
|
<li><a href={'#'} class="is-disabled">No chats yet...</a></li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
{#key $checkStateChange}
|
||||||
<ul>
|
{#each sortedChats as chat, i}
|
||||||
{#each sortedChats as chat}
|
<ChatMenuItem activeChatId={activeChatId} chat={chat} prevChat={sortedChats[i - 1]} nextChat={sortedChats[i + 1]} />
|
||||||
<li>
|
|
||||||
<a style="position: relative" href={`#/chat/${chat.id}`} class:is-disabled={!$apiKeyStorage} class:is-active={activeChatId === chat.id}>
|
|
||||||
<a class="is-pulled-right is-hidden px-1 py-0 greyscale has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat(chat.id)}>🗑️</a>
|
|
||||||
{chat.name || `Chat ${chat.id}`}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
{/key}
|
||||||
</li>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Actions</p>
|
<!-- <p class="menu-label">Actions</p> -->
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<a href={'#/'} class="panel-block" class:is-disabled={!$apiKeyStorage} class:is-active={!activeChatId}
|
<div class="level-right side-actions">
|
||||||
><span class="greyscale mr-2">🔑</span> API key</a
|
{#if !$apiKeyStorage}
|
||||||
>
|
<div class="level-item">
|
||||||
</li>
|
<a href={'#/'} class="panel-block" class:is-disabled={!$apiKeyStorage}
|
||||||
<li>
|
><span class="greyscale mr-2"><Fa icon={faKey} /></span> API key</a
|
||||||
<a href={'#/chat/new'} class="panel-block" class:is-disabled={!$apiKeyStorage}
|
></div>
|
||||||
><span class="greyscale mr-2">➕</span> New chat</a
|
{:else}
|
||||||
>
|
<div class="level-item">
|
||||||
</li>
|
<button on:click={() => { startNewChatWithWarning(activeChatId) }} class="panel-block button" title="Start new chat with default profile" class:is-disabled={!$apiKeyStorage}
|
||||||
<li>
|
><span class="greyscale mr-2"><Fa icon={faSquarePlus} /></span> New chat</button>
|
||||||
<a class="panel-block"
|
</div>
|
||||||
href="{'#/'}"
|
|
||||||
class:is-disabled={!$apiKeyStorage}
|
|
||||||
on:click|preventDefault={() => {
|
|
||||||
const confirmDelete = window.confirm('Are you sure you want to delete all your chats?')
|
|
||||||
if (confirmDelete) {
|
|
||||||
replace('#/').then(() => clearChats())
|
|
||||||
}
|
|
||||||
}}><span class="greyscale mr-2">🗑️</span> Clear chats</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{#if activeChatId}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={'#/'}
|
|
||||||
class="panel-block"
|
|
||||||
class:is-disabled={!apiKeyStorage}
|
|
||||||
on:click|preventDefault={() => {
|
|
||||||
if (activeChatId) {
|
|
||||||
exportAsMarkdown(activeChatId)
|
|
||||||
}
|
|
||||||
}}><span class="greyscale mr-2">📥</span> Export chat</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import type { Message, Model, Usage } from './Types.svelte'
|
||||||
|
import { encode } from 'gpt-tokenizer'
|
||||||
|
|
||||||
|
// Reference: https://openai.com/pricing#language-models
|
||||||
|
// TODO: Move to settings of some type
|
||||||
|
const modelDetails : Record<string, [number, number, number]> = {
|
||||||
|
'gpt-4-32k': [0.00006, 0.00012, 32768], // $0.06 per 1000 tokens prompt, $0.12 per 1000 tokens completion, max 32k
|
||||||
|
'gpt-4': [0.00003, 0.00006, 8192], // $0.03 per 1000 tokens prompt, $0.06 per 1000 tokens completion, max 8k
|
||||||
|
'gpt-3.5': [0.000002, 0.000002, 4096] // $0.002 per 1000 tokens (both prompt and completion), max 4k
|
||||||
|
}
|
||||||
|
|
||||||
|
const tpCache = {}
|
||||||
|
const getModelDetail = (model: Model) => {
|
||||||
|
let r = tpCache[model]
|
||||||
|
if (r) return r
|
||||||
|
const k = Object.keys(modelDetails).find((k) => model.startsWith(k))
|
||||||
|
if (k) {
|
||||||
|
r = modelDetails[k]
|
||||||
|
} else {
|
||||||
|
r = [0, 0, 4096]
|
||||||
|
}
|
||||||
|
tpCache[model] = r
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPrice = (tokens: Usage, model: Model): number => {
|
||||||
|
const t = getModelDetail(model)
|
||||||
|
return ((tokens.prompt_tokens * t[0]) + (tokens.completion_tokens * t[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const countPromptTokens = (prompts:Message[], model:Model):number => {
|
||||||
|
return prompts.reduce((a, m) => {
|
||||||
|
// Not sure how OpenAI formats it, but this seems to get close to the right counts.
|
||||||
|
// Would be nice to know. This works for gpt-3.5. gpt-4 could be different
|
||||||
|
a += encode('## ' + m.role + ' ##:\r\n\r\n' + m.content + '\r\n\r\n\r\n').length
|
||||||
|
return a
|
||||||
|
}, 0) + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModelMaxTokens = (model:Model):number => {
|
||||||
|
return getModelDetail(model)[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,43 +1,287 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { persisted } from 'svelte-local-storage-store'
|
import { persisted } from 'svelte-local-storage-store'
|
||||||
import { get } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
import type { Chat, Message } from './Types.svelte'
|
import type { Chat, ChatSettings, GlobalSettings, Message, ChatSetting, GlobalSetting, Usage, Model } from './Types.svelte'
|
||||||
|
import { getChatSettingObjectByKey, getGlobalSettingObjectByKey, getChatDefaults, getExcludeFromProfile } from './Settings.svelte'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getProfile, getProfiles, isStaticProfile, newNameForProfile, restartProfile } from './Profiles.svelte'
|
||||||
|
import { errorNotice } from './Util.svelte'
|
||||||
|
|
||||||
export const chatsStorage = persisted('chats', [] as Chat[])
|
export const chatsStorage = persisted('chats', [] as Chat[])
|
||||||
|
export const latestModelMap = persisted('latestModelMap', {} as Record<Model, Model>) // What was returned when a model was requested
|
||||||
|
export const globalStorage = persisted('global', {} as GlobalSettings)
|
||||||
export const apiKeyStorage = persisted('apiKey', '' as string)
|
export const apiKeyStorage = persisted('apiKey', '' as string)
|
||||||
|
export let checkStateChange = writable(0) // Trigger for Chat
|
||||||
|
export let showSetChatSettings = writable(false) //
|
||||||
|
export let submitExitingPromptsNow = writable(false) // for them to go now. Will not submit anything in the input
|
||||||
|
export let pinMainMenu = writable(false) // Show menu (for mobile use)
|
||||||
|
export let continueMessage = writable('') //
|
||||||
|
|
||||||
export const addChat = (): number => {
|
const chatDefaults = getChatDefaults()
|
||||||
|
|
||||||
|
export const newChatID = (): number => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1
|
||||||
|
return chatId
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addChat = (profile:ChatSettings|undefined = undefined): number => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
// Find the max chatId
|
// Find the max chatId
|
||||||
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1
|
const chatId = newChatID()
|
||||||
|
|
||||||
|
profile = JSON.parse(JSON.stringify(profile || getProfile(''))) as ChatSettings
|
||||||
|
|
||||||
// Add a new chat
|
// Add a new chat
|
||||||
chats.push({
|
chats.push({
|
||||||
id: chatId,
|
id: chatId,
|
||||||
name: `Chat ${chatId}`,
|
name: `Chat ${chatId}`,
|
||||||
messages: []
|
settings: profile,
|
||||||
|
messages: [],
|
||||||
|
usage: {} as Record<Model, Usage>,
|
||||||
|
startSession: false,
|
||||||
|
sessionStarted: false
|
||||||
})
|
})
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
|
// Apply defaults and prepare it to start
|
||||||
|
restartProfile(chatId)
|
||||||
return chatId
|
return chatId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addChatFromJSON = (json: string): number => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
|
// Find the max chatId
|
||||||
|
const chatId = newChatID()
|
||||||
|
|
||||||
|
let chat: Chat
|
||||||
|
try {
|
||||||
|
chat = JSON.parse(json) as Chat
|
||||||
|
if (!chat.settings || !chat.messages || isNaN(chat.id)) {
|
||||||
|
errorNotice('Not valid Chat JSON')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorNotice("Can't parse file JSON")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.id = chatId
|
||||||
|
|
||||||
|
// Add a new chat
|
||||||
|
chats.push(chat)
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
// make sure it's up-to-date
|
||||||
|
updateChatSettings(chatId)
|
||||||
|
return chatId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure a chat's settings are set with current values or defaults
|
||||||
|
export const updateChatSettings = (chatId:number) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
if (!chat.settings) {
|
||||||
|
chat.settings = {} as ChatSettings
|
||||||
|
}
|
||||||
|
updateProfile(chat.settings, false)
|
||||||
|
// make sure old chat messages have UUID
|
||||||
|
chat.messages.forEach((m) => {
|
||||||
|
m.uuid = m.uuid || uuidv4()
|
||||||
|
delete m.streaming
|
||||||
|
})
|
||||||
|
// Make sure the usage totals object is set
|
||||||
|
// (some earlier versions of this had different structures)
|
||||||
|
const hasUsage = chat.usage && !Array.isArray(chat.usage) &&
|
||||||
|
typeof chat.usage === 'object' &&
|
||||||
|
Object.values(chat.usage).find(v => 'prompt_tokens' in v)
|
||||||
|
if (!hasUsage) {
|
||||||
|
const usageMap:Record<Model, Usage> = {}
|
||||||
|
chat.usage = usageMap
|
||||||
|
}
|
||||||
|
if (chat.startSession === undefined) chat.startSession = false
|
||||||
|
if (chat.sessionStarted === undefined) chat.sessionStarted = !!chat.messages.find(m => m.role === 'user')
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure profile options are set with current values or defaults
|
||||||
|
export const updateProfile = (profile:ChatSettings, exclude:boolean):ChatSettings => {
|
||||||
|
Object.entries(getChatDefaults()).forEach(([k, v]) => {
|
||||||
|
const val = profile[k]
|
||||||
|
profile[k] = (val === undefined || val === null ? v : profile[k])
|
||||||
|
})
|
||||||
|
// update old useSummarization to continuousChat mode setting
|
||||||
|
if ('useSummarization' in profile || !('continuousChat' in profile)) {
|
||||||
|
const usm = profile.useSummarization
|
||||||
|
if (usm && !profile.summaryPrompt) {
|
||||||
|
profile.continuousChat = 'fifo'
|
||||||
|
} else if (usm) {
|
||||||
|
profile.continuousChat = 'summary'
|
||||||
|
} else {
|
||||||
|
profile.continuousChat = ''
|
||||||
|
}
|
||||||
|
delete profile.useSummarization
|
||||||
|
}
|
||||||
|
if (exclude) {
|
||||||
|
Object.keys(getExcludeFromProfile()).forEach(k => {
|
||||||
|
delete profile[k]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all setting to current profile defaults
|
||||||
|
export const resetChatSettings = (chatId, resetAll:boolean = false) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const profile = getProfile(chat.settings.profile)
|
||||||
|
const exclude = getExcludeFromProfile()
|
||||||
|
if (resetAll) {
|
||||||
|
// Reset to base defaults first, then apply profile
|
||||||
|
Object.entries(getChatDefaults()).forEach(([k, v]) => {
|
||||||
|
chat.settings[k] = v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Object.entries(profile).forEach(([k, v]) => {
|
||||||
|
if (exclude[k]) return
|
||||||
|
chat.settings[k] = v
|
||||||
|
})
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
export const clearChats = () => {
|
export const clearChats = () => {
|
||||||
chatsStorage.set([])
|
chatsStorage.set([])
|
||||||
}
|
}
|
||||||
|
export const saveChatStore = () => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChat = (chatId: number):Chat => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
return chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChatSettings = (chatId: number):ChatSettings => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
return (chats.find((chat) => chat.id === chatId) as Chat).settings
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRunningTotal = (chatId: number, usage: Usage, model:Model) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
let total:Usage = chat.usage[model]
|
||||||
|
if (!total) {
|
||||||
|
total = {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 0,
|
||||||
|
total_tokens: 0
|
||||||
|
}
|
||||||
|
chat.usage[model] = total
|
||||||
|
}
|
||||||
|
total.completion_tokens += usage.completion_tokens
|
||||||
|
total.prompt_tokens += usage.prompt_tokens
|
||||||
|
total.total_tokens += usage.total_tokens
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subtractRunningTotal = (chatId: number, usage: Usage, model:Model) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
let total:Usage = chat.usage[model]
|
||||||
|
if (!total) {
|
||||||
|
total = {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 0,
|
||||||
|
total_tokens: 0
|
||||||
|
}
|
||||||
|
chat.usage[model] = total
|
||||||
|
}
|
||||||
|
total.completion_tokens -= usage.completion_tokens
|
||||||
|
total.prompt_tokens -= usage.prompt_tokens
|
||||||
|
total.total_tokens -= usage.total_tokens
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
export const addMessage = (chatId: number, message: Message) => {
|
export const addMessage = (chatId: number, message: Message) => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
if (!message.uuid) message.uuid = uuidv4()
|
||||||
|
if (chat.messages.indexOf(message) < 0) {
|
||||||
|
// Don't have message, add it
|
||||||
chat.messages.push(message)
|
chat.messages.push(message)
|
||||||
|
}
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const editMessage = (chatId: number, index: number, newMessage: Message) => {
|
export const getMessages = (chatId: number):Message[] => {
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
chat.messages[index] = newMessage
|
return chat.messages
|
||||||
chat.messages.splice(index + 1) // remove the rest of the messages
|
}
|
||||||
|
|
||||||
|
export const getMessage = (chat: Chat, uuid:string):Message|undefined => {
|
||||||
|
return chat.messages.find((m) => m.uuid === uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertMessages = (chatId: number, insertAfter: Message, newMessages: Message[]) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const index = chat.messages.findIndex((m) => m.uuid === insertAfter.uuid)
|
||||||
|
if (index === undefined || index < 0) {
|
||||||
|
console.error("Couldn't insert after message:", insertAfter)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chat.messages.splice(index + 1, 0, ...newMessages)
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSummaryMessage = (chatId: number, uuid: string) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const message = getMessage(chat, uuid)
|
||||||
|
if (message && message.summarized) throw new Error('Unable to delete summarized message')
|
||||||
|
if (message && message.summary) { // messages we summarized
|
||||||
|
message.summary.forEach(sid => {
|
||||||
|
const m = getMessage(chat, sid)
|
||||||
|
if (m) {
|
||||||
|
delete m.summarized // unbind to this summary
|
||||||
|
}
|
||||||
|
})
|
||||||
|
delete message.summary
|
||||||
|
}
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
deleteMessage(chatId, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMessage = (chatId: number, uuid: string) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const index = chat.messages.findIndex((m) => m.uuid === uuid)
|
||||||
|
const message = getMessage(chat, uuid)
|
||||||
|
if (message && message.summarized) throw new Error('Unable to delete summarized message')
|
||||||
|
if (message && message.summary) throw new Error('Unable to directly delete message summary')
|
||||||
|
// const found = chat.messages.filter((m) => m.uuid === uuid)
|
||||||
|
if (index < 0) {
|
||||||
|
console.error(`Unable to find and delete message with ID: ${uuid}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// console.warn(`Deleting message with ID: ${uuid}`, found, index)
|
||||||
|
chat.messages.splice(index, 1) // remove item
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const truncateFromMessage = (chatId: number, uuid: string) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const index = chat.messages.findIndex((m) => m.uuid === uuid)
|
||||||
|
const message = getMessage(chat, uuid)
|
||||||
|
if (message && message.summarized) throw new Error('Unable to truncate from a summarized message')
|
||||||
|
// const found = chat.messages.filter((m) => m.uuid === uuid)
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error(`Unable to find message with ID: ${uuid}`)
|
||||||
|
}
|
||||||
|
chat.messages.splice(index + 1) // remove every item after
|
||||||
chatsStorage.set(chats)
|
chatsStorage.set(chats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,4 +296,184 @@
|
||||||
const chats = get(chatsStorage)
|
const chats = get(chatsStorage)
|
||||||
chatsStorage.set(chats.filter((chat) => chat.id !== chatId))
|
chatsStorage.set(chats.filter((chat) => chat.id !== chatId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const copyChat = (chatId: number) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const nameMap = chats.reduce((a, chat) => { a[chat.name] = chat; return a }, {})
|
||||||
|
let i:number = 1
|
||||||
|
let cname = chat.name + `-${i}`
|
||||||
|
while (nameMap[cname]) {
|
||||||
|
i++
|
||||||
|
cname = chat.name + `-${i}`
|
||||||
|
}
|
||||||
|
const chatCopy = JSON.parse(JSON.stringify(chat))
|
||||||
|
|
||||||
|
// Set the ID
|
||||||
|
chatCopy.id = newChatID()
|
||||||
|
// Set new name
|
||||||
|
chatCopy.name = cname
|
||||||
|
|
||||||
|
// Add a new chat
|
||||||
|
chats.push(chatCopy)
|
||||||
|
|
||||||
|
// chatsStorage
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanSettingValue = (type:string, value: any) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'number':
|
||||||
|
value = parseFloat(value)
|
||||||
|
if (isNaN(value)) { value = null }
|
||||||
|
return value
|
||||||
|
case 'boolean':
|
||||||
|
if (typeof value === 'string') value = value.trim().toLocaleLowerCase()
|
||||||
|
return value === 'true' || value === 'yes' || (value ? value !== 'false' && value !== 'no' && !!value : false)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setChatSettingValueByKey = (chatId: number, key: keyof ChatSettings, value) => {
|
||||||
|
const setting = getChatSettingObjectByKey(key)
|
||||||
|
if (setting) return setChatSettingValue(chatId, setting, value)
|
||||||
|
if (!(key in chatDefaults)) throw new Error('Invalid chat setting: ' + key)
|
||||||
|
const d = chatDefaults[key]
|
||||||
|
if (d === null || d === undefined) {
|
||||||
|
throw new Error('Unable to determine setting type for "' +
|
||||||
|
key + ' from default of "' + d + '"')
|
||||||
|
}
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const settings = chat.settings as any
|
||||||
|
settings[key] = cleanSettingValue(typeof d, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setChatSettingValue = (chatId: number, setting: ChatSetting, value) => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
let settings = chat.settings as any
|
||||||
|
if (!settings) {
|
||||||
|
settings = {} as ChatSettings
|
||||||
|
chat.settings = settings
|
||||||
|
}
|
||||||
|
settings[setting.key] = cleanSettingValue(setting.type, value)
|
||||||
|
chatsStorage.set(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChatSettingValueNullDefault = (chatId: number, setting: ChatSetting):any => {
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
let value = chat.settings && chat.settings[setting.key]
|
||||||
|
value = (value === undefined) ? null : value
|
||||||
|
if (!setting.forceApi && value === chatDefaults[setting.key]) value = null
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setGlobalSettingValueByKey = (key: keyof GlobalSettings, value) => {
|
||||||
|
return setGlobalSettingValue(getGlobalSettingObjectByKey(key), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setGlobalSettingValue = (setting: GlobalSetting, value) => {
|
||||||
|
const store = get(globalStorage)
|
||||||
|
store[setting.key as any] = cleanSettingValue(setting.type, value)
|
||||||
|
globalStorage.set(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getGlobalSettingValue = (key:keyof GlobalSetting, value):any => {
|
||||||
|
const store = get(globalStorage)
|
||||||
|
return store[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGlobalSettings = ():GlobalSettings => {
|
||||||
|
return get(globalStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomProfiles = ():Record<string, ChatSettings> => {
|
||||||
|
const store = get(globalStorage)
|
||||||
|
return store.profiles || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteCustomProfile = (chatId:number, profileId:string) => {
|
||||||
|
if (isStaticProfile(profileId as any)) {
|
||||||
|
throw new Error('Sorry, you can\'t delete a static profile.')
|
||||||
|
}
|
||||||
|
const chats = get(chatsStorage)
|
||||||
|
const chat = chats.find((chat) => chat.id === chatId) as Chat
|
||||||
|
const store = get(globalStorage)
|
||||||
|
if (store.defaultProfile === chat.settings.profile) {
|
||||||
|
throw new Error('Sorry, you can\'t delete the default profile.')
|
||||||
|
}
|
||||||
|
delete store.profiles[profileId]
|
||||||
|
globalStorage.set(store)
|
||||||
|
getProfiles(true) // force update profile cache
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveCustomProfile = (profile:ChatSettings) => {
|
||||||
|
const store = get(globalStorage)
|
||||||
|
let profiles = store.profiles
|
||||||
|
if (!profiles) {
|
||||||
|
profiles = {}
|
||||||
|
store.profiles = profiles
|
||||||
|
}
|
||||||
|
if (!profile.profile) profile.profile = uuidv4()
|
||||||
|
const mt = profile.profileName && profile.profileName.trim().toLocaleLowerCase()
|
||||||
|
const sameTitle = Object.values(profiles).find(c => c.profile !== profile.profile &&
|
||||||
|
c.profileName && c.profileName.trim().toLocaleLowerCase() === mt)
|
||||||
|
if (sameTitle) {
|
||||||
|
throw new Error(`Sorry, another profile already exists with the name "${profile.profileName}"`)
|
||||||
|
}
|
||||||
|
if (!mt) {
|
||||||
|
throw new Error('Sorry, you need to enter a valid name for your profile.')
|
||||||
|
}
|
||||||
|
if (!profile.characterName || profile.characterName.length < 3) {
|
||||||
|
throw new Error('Your profile\'s character needs a valid name.')
|
||||||
|
}
|
||||||
|
if (isStaticProfile(profile.profile as any)) {
|
||||||
|
// throw new Error('Sorry, you can\'t modify a static profile. You can clone it though!')
|
||||||
|
// Save static profile as new custom
|
||||||
|
profile.profileName = newNameForProfile(profile.profileName)
|
||||||
|
profile.profile = uuidv4()
|
||||||
|
}
|
||||||
|
const clone = JSON.parse(JSON.stringify(profile)) // Always store a copy
|
||||||
|
// pull excluded
|
||||||
|
Object.keys(getExcludeFromProfile()).forEach(k => {
|
||||||
|
delete clone[k]
|
||||||
|
})
|
||||||
|
// pull defaults
|
||||||
|
// Object.entries(getChatDefaults()).forEach(([k, v]) => {
|
||||||
|
// if (clone[k] === v || (v === undefined && clone[k] === null)) delete clone[k]
|
||||||
|
// })
|
||||||
|
profiles[profile.profile as string] = clone
|
||||||
|
globalStorage.set(store)
|
||||||
|
profile.isDirty = false
|
||||||
|
saveChatStore()
|
||||||
|
getProfiles(true) // force update profile cache
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newName = (name:string, nameMap:Record<string, any>):string => {
|
||||||
|
if (!nameMap[name]) return name
|
||||||
|
let i:number = 1
|
||||||
|
let cname = name + `-${i}`
|
||||||
|
while (nameMap[cname]) {
|
||||||
|
i++
|
||||||
|
cname = name + `-${i}`
|
||||||
|
}
|
||||||
|
return cname
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLatestKnownModel = (model:Model) => {
|
||||||
|
const modelMapStore = get(latestModelMap)
|
||||||
|
return modelMapStore[model] || model
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setLatestKnownModel = (requestedModel:Model, responseModel:Model) => {
|
||||||
|
const modelMapStore = get(latestModelMap)
|
||||||
|
modelMapStore[requestedModel] = responseModel
|
||||||
|
latestModelMap.set(modelMapStore)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
|
// import type internal from "stream";
|
||||||
|
|
||||||
export const supportedModels = [ // See: https://platform.openai.com/docs/models/model-endpoint-compatibility
|
export const supportedModels = [ // See: https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
'gpt-4-0314',
|
'gpt-4-0314',
|
||||||
|
@ -18,19 +20,26 @@
|
||||||
export type Message = {
|
export type Message = {
|
||||||
role: 'user' | 'assistant' | 'system' | 'error';
|
role: 'user' | 'assistant' | 'system' | 'error';
|
||||||
content: string;
|
content: string;
|
||||||
|
uuid: string;
|
||||||
usage?: Usage;
|
usage?: Usage;
|
||||||
model?: Model;
|
model?: Model;
|
||||||
|
removed?: boolean;
|
||||||
|
summarized?: string[];
|
||||||
|
summary?: string[];
|
||||||
|
suppress?: boolean;
|
||||||
|
finish_reason?: string;
|
||||||
|
streaming?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Chat = {
|
export type ResponseAlteration = {
|
||||||
id: number;
|
type: 'prompt' | 'replace';
|
||||||
name: string;
|
match: string;
|
||||||
messages: Message[];
|
replace: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Request = {
|
export type Request = {
|
||||||
model?: Model;
|
model?: Model;
|
||||||
messages: Message[];
|
messages?: Message[];
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
top_p?: number;
|
top_p?: number;
|
||||||
n?: number;
|
n?: number;
|
||||||
|
@ -39,29 +48,40 @@
|
||||||
max_tokens?: number;
|
max_tokens?: number;
|
||||||
presence_penalty?: number;
|
presence_penalty?: number;
|
||||||
frequency_penalty?: number;
|
frequency_penalty?: number;
|
||||||
logit_bias?: Record<string, any>;
|
logit_bias?: Record<string, any> | null;
|
||||||
user?: string;
|
user?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsNumber = {
|
export type ChatSettings = {
|
||||||
type: 'number';
|
profile: string,
|
||||||
default: number;
|
characterName: string,
|
||||||
min: number;
|
profileName: string,
|
||||||
max: number;
|
profileDescription: string,
|
||||||
step: number;
|
continuousChat: (''|'fifo'|'summary');
|
||||||
};
|
// useSummarization: boolean;
|
||||||
|
summaryThreshold: number;
|
||||||
|
summarySize: number;
|
||||||
|
pinTop: number;
|
||||||
|
pinBottom: number;
|
||||||
|
summaryPrompt: string;
|
||||||
|
useSystemPrompt: boolean;
|
||||||
|
systemPrompt: string;
|
||||||
|
autoStartSession: boolean;
|
||||||
|
trainingPrompts?: Message[];
|
||||||
|
useResponseAlteration?: boolean;
|
||||||
|
responseAlterations?: ResponseAlteration[];
|
||||||
|
isDirty?: boolean;
|
||||||
|
} & Request;
|
||||||
|
|
||||||
export type SettingsSelect = {
|
export type Chat = {
|
||||||
type: 'select';
|
id: number;
|
||||||
default: Model;
|
|
||||||
options: Model[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Settings = {
|
|
||||||
key: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
messages: Message[];
|
||||||
} & (SettingsNumber | SettingsSelect);
|
usage: Record<Model, Usage>;
|
||||||
|
settings: ChatSettings;
|
||||||
|
startSession: boolean;
|
||||||
|
sessionStarted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ResponseOK = {
|
type ResponseOK = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -71,6 +91,7 @@
|
||||||
index: number;
|
index: number;
|
||||||
message: Message;
|
message: Message;
|
||||||
finish_reason: string;
|
finish_reason: string;
|
||||||
|
delta: Message;
|
||||||
}[];
|
}[];
|
||||||
usage: Usage;
|
usage: Usage;
|
||||||
model: Model;
|
model: Model;
|
||||||
|
@ -93,4 +114,112 @@
|
||||||
id: string;
|
id: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChatCompletionOpts = {
|
||||||
|
chat: Chat;
|
||||||
|
autoAddMessages: boolean;
|
||||||
|
maxTokens?:number;
|
||||||
|
summaryRequest?:boolean;
|
||||||
|
didSummary?:boolean;
|
||||||
|
streaming?:boolean;
|
||||||
|
onMessageChange?: (messages: Message[]) => void;
|
||||||
|
fillMessage?:Message,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GlobalSettings = {
|
||||||
|
profiles: Record<string, ChatSettings>;
|
||||||
|
lastProfile?: string;
|
||||||
|
defaultProfile?: string;
|
||||||
|
hideSummarized?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingNumber = {
|
||||||
|
type: 'number';
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectOption = {
|
||||||
|
value: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingBoolean = {
|
||||||
|
type: 'boolean';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingSelect = {
|
||||||
|
type: 'select';
|
||||||
|
options: SelectOption[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingText = {
|
||||||
|
type: 'text';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingTextArea = {
|
||||||
|
type: 'textarea';
|
||||||
|
lines?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingOther = {
|
||||||
|
type: 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ControlAction = {
|
||||||
|
title:string;
|
||||||
|
icon?:any,
|
||||||
|
text?:string;
|
||||||
|
class?:string;
|
||||||
|
disabled?:boolean;
|
||||||
|
action?: (chatId:number, setting:any, value:any) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FieldControl = {
|
||||||
|
getAction: (chatId:number, setting:any, value:any) => ControlAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubSetting = {
|
||||||
|
type: 'subset';
|
||||||
|
settings: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatSetting = {
|
||||||
|
key: keyof ChatSettings;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
forceApi?: boolean; // force in api requests, even if set to default
|
||||||
|
hidden?: boolean; // Hide from setting menus
|
||||||
|
header?: string;
|
||||||
|
headerClass?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
hide?: (chatId:number) => boolean;
|
||||||
|
apiTransform?: (chatId:number, setting:ChatSetting, value:any) => any;
|
||||||
|
fieldControls?: FieldControl[];
|
||||||
|
beforeChange?: (chatId:number, setting:ChatSetting, value:any) => boolean;
|
||||||
|
afterChange?: (chatId:number, setting:ChatSetting, value:any) => boolean;
|
||||||
|
} & (SettingNumber | SettingSelect | SettingBoolean | SettingText | SettingTextArea | SettingOther | SubSetting);
|
||||||
|
|
||||||
|
|
||||||
|
export type GlobalSetting = {
|
||||||
|
key: keyof GlobalSettings;
|
||||||
|
name?: string;
|
||||||
|
title?: string;
|
||||||
|
required?: boolean; // force in request
|
||||||
|
hidden?: boolean; // Hide from setting menus
|
||||||
|
header?: string;
|
||||||
|
headerClass?: string;
|
||||||
|
} & (SettingNumber | SettingSelect | SettingBoolean | SettingText | SettingOther);
|
||||||
|
|
||||||
|
export type SettingPrompt = {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
class?: string;
|
||||||
|
checkPrompt: (setting:ChatSetting, newVal:any, oldVal:any)=>boolean;
|
||||||
|
onYes?: (setting:ChatSetting, newVal:any, oldVal:any)=>boolean;
|
||||||
|
onNo?: (setting:ChatSetting, newVal:any, oldVal:any)=>boolean;
|
||||||
|
passed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import { compare } from 'stacking-order'
|
||||||
|
import { openModal } from 'svelte-modals'
|
||||||
|
import PromptNotice from './PromptNotice.svelte'
|
||||||
|
import { getChat } from './Storage.svelte'
|
||||||
|
import { replace } from 'svelte-spa-router'
|
||||||
|
import PromptConfirm from './PromptConfirm.svelte'
|
||||||
|
export const sizeTextElements = () => {
|
||||||
|
const els = document.querySelectorAll('textarea.auto-size')
|
||||||
|
for (let i:number = 0, l = els.length; i < l; i++) autoGrowInput(els[i] as HTMLTextAreaElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const autoGrowInputOnEvent = (event: Event) => {
|
||||||
|
// Resize the textarea to fit the content - auto is important to reset the height after deleting content
|
||||||
|
if (event.target === null) return
|
||||||
|
(event.target as any).__didAutoGrow = false
|
||||||
|
autoGrowInput(event.target as HTMLTextAreaElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const autoGrowInput = (el: HTMLTextAreaElement) => {
|
||||||
|
const anyEl = el as any // Oh how I hate typescript. All the markup of Java with no real payoff..
|
||||||
|
if (!anyEl.__didAutoGrow) el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
|
||||||
|
el.style.height = el.scrollHeight + 'px'
|
||||||
|
anyEl.__didAutoGrow = true // don't resize this one again unless it's via an event
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scrollIntoViewWithOffset = (element:HTMLElement, offset:number, instant:boolean = false, bottom:boolean = false) => {
|
||||||
|
const behavior = instant ? 'instant' : 'smooth'
|
||||||
|
if (bottom) {
|
||||||
|
window.scrollTo({
|
||||||
|
behavior: behavior as any,
|
||||||
|
top:
|
||||||
|
(element.getBoundingClientRect().bottom) -
|
||||||
|
document.body.getBoundingClientRect().top - (window.innerHeight - offset)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
window.scrollTo({
|
||||||
|
behavior: behavior as any,
|
||||||
|
top:
|
||||||
|
element.getBoundingClientRect().top -
|
||||||
|
document.body.getBoundingClientRect().top -
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scrollToMessage = (uuid:string | string[] | undefined, offset:number = 60, instant:boolean = false, bottom:boolean = false) => {
|
||||||
|
if (Array.isArray(uuid)) {
|
||||||
|
uuid = uuid[0]
|
||||||
|
}
|
||||||
|
if (!uuid) {
|
||||||
|
console.error('Not a valid uuid', uuid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const el = document.getElementById('message-' + uuid)
|
||||||
|
if (el) {
|
||||||
|
scrollIntoViewWithOffset(el, offset, instant, bottom)
|
||||||
|
} else {
|
||||||
|
console.error("Can't find element with message ID", uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkModalEsc = (event:KeyboardEvent|undefined):boolean|void => {
|
||||||
|
if (!event || event.key !== 'Escape') return
|
||||||
|
dispatchModalEsc()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dispatchModalEsc = ():boolean|void => {
|
||||||
|
const stack = Array.from(document.querySelectorAll('.modal, .has-esc')).filter(s =>
|
||||||
|
window.getComputedStyle(s).getPropertyValue('display') !== 'none'
|
||||||
|
)
|
||||||
|
const top:HTMLElement = stack.length === 1
|
||||||
|
? stack[0]
|
||||||
|
: stack.find(m1 => {
|
||||||
|
return stack.find(m2 => {
|
||||||
|
return m1 !== m2 && compare(m1, m2) > 0 && m1
|
||||||
|
})
|
||||||
|
}) as any
|
||||||
|
if (top) {
|
||||||
|
// trigger modal-esc event on topmost modal when esc key is pressed
|
||||||
|
const e = new CustomEvent('modal-esc', { detail: top })
|
||||||
|
top.dispatchEvent(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorNotice = (message:string, error:Error|undefined = undefined):any => {
|
||||||
|
openModal(PromptNotice, {
|
||||||
|
title: 'Error',
|
||||||
|
class: 'is-danger',
|
||||||
|
message: message + (error ? '<br>' + error.message : ''),
|
||||||
|
asHtml: true,
|
||||||
|
onConfirm: () => {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const warningNotice = (message:string, error:Error|undefined = undefined):any => {
|
||||||
|
openModal(PromptNotice, {
|
||||||
|
title: 'Warning',
|
||||||
|
class: 'is-warning',
|
||||||
|
message: message + (error ? '<br>' + error.message : ''),
|
||||||
|
asHtml: true,
|
||||||
|
onConfirm: () => {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const startNewChatWithWarning = (activeChatId: number|undefined) => {
|
||||||
|
if (activeChatId && getChat(activeChatId).settings.isDirty) {
|
||||||
|
openModal(PromptConfirm, {
|
||||||
|
title: 'Unsaved Profile',
|
||||||
|
message: '<p>There are unsaved changes to your current profile that will be lost.</p><p>Discard these changes and continue with new chat?</p>',
|
||||||
|
asHtml: true,
|
||||||
|
class: 'is-warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
replace('#/chat/new')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
replace('#/chat/new')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -21,7 +21,8 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
base: './'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue