diff --git a/package.json b/package.json index c95df8a..f4fdd91 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "shibuya", "author": { - "name": "Bohan Cheng", - "email": "cbh778899@outlook.com" + "name": "Bohan Cheng", + "email": "cbh778899@outlook.com" }, "version": "0.1.11", "main": "electron.js", @@ -20,6 +20,7 @@ "@aws-sdk/credential-providers": "^3.650.0", "@huggingface/jinja": "^0.3.0", "@wllama/wllama": "^1.16.0", + "openai": "^4.61.0", "react": "^18.3.1", "react-bootstrap-icons": "^1.11.4", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b14b16a..3cd601e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@wllama/wllama': specifier: ^1.16.0 version: 1.16.1 + openai: + specifier: ^4.61.0 + version: 4.61.0(encoding@0.1.13) react: specifier: ^18.3.1 version: 18.3.1 @@ -901,6 +904,12 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/node-fetch@2.6.11': + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + + '@types/node@18.19.50': + resolution: {integrity: sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==} + '@types/node@20.16.1': resolution: {integrity: sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==} @@ -913,6 +922,9 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/qs@6.9.16': + resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} + '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -953,6 +965,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1551,6 +1567,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} @@ -1610,10 +1630,17 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2318,6 +2345,19 @@ packages: node-api-version@0.2.0: resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-gyp@9.4.1: resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} engines: {node: ^12.13 || ^14.13 || >=16} @@ -2379,6 +2419,15 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@4.61.0: + resolution: {integrity: sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==} + hasBin: true + peerDependencies: + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2490,6 +2539,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2836,6 +2889,9 @@ packages: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2880,6 +2936,9 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -2980,6 +3039,16 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -4335,6 +4404,15 @@ snapshots: '@types/ms@0.7.34': {} + '@types/node-fetch@2.6.11': + dependencies: + '@types/node': 22.4.2 + form-data: 4.0.0 + + '@types/node@18.19.50': + dependencies: + undici-types: 5.26.5 + '@types/node@20.16.1': dependencies: undici-types: 6.19.8 @@ -4351,6 +4429,8 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/qs@6.9.16': {} + '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.5 @@ -4395,6 +4475,10 @@ snapshots: abbrev@1.1.1: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -5253,6 +5337,8 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + exponential-backoff@3.1.1: {} extend@3.0.2: {} @@ -5317,12 +5403,19 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data-encoder@1.7.2: {} + form-data@4.0.0: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -6182,6 +6275,14 @@ snapshots: dependencies: semver: 7.6.3 + node-domexception@1.0.0: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + node-gyp@9.4.1: dependencies: env-paths: 2.2.1 @@ -6256,6 +6357,20 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@4.61.0(encoding@0.1.13): + dependencies: + '@types/node': 18.19.50 + '@types/node-fetch': 2.6.11 + '@types/qs': 6.9.16 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + qs: 6.13.0 + transitivePeerDependencies: + - encoding + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6369,6 +6484,10 @@ snapshots: punycode@2.3.1: {} + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -6816,6 +6935,8 @@ snapshots: to-fast-properties@2.0.0: {} + tr46@0.0.3: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -6874,6 +6995,8 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + undici-types@5.26.5: {} + undici-types@6.19.8: {} unified@11.0.5: @@ -6968,6 +7091,15 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 diff --git a/src/components/chat/Conversation.jsx b/src/components/chat/Conversation.jsx index 792103f..4ad1894 100644 --- a/src/components/chat/Conversation.jsx +++ b/src/components/chat/Conversation.jsx @@ -4,9 +4,10 @@ import { FileImageFill, FileTextFill, Paperclip, Send, StopCircleFill } from 're import useIDB from "../../utils/idb"; import { isModelLoaded, loadModel } from '../../utils/workers/worker' import { getCompletionFunctions } from "../../utils/workers"; -import { setClient } from "../../utils/workers/aws-worker"; +import { setClient as setAwsClient } from "../../utils/workers/aws-worker"; +import { setClient as setOpenaiClient } from "../../utils/workers/openai-worker"; -export default function Conversation({ uid, client }) { +export default function Conversation({ uid, client, updateClient }) { const [conversation, setConversation] = useState([]); const [message, setMessage] = useState(''); @@ -130,15 +131,21 @@ export default function Conversation({ uid, client }) { useEffect(()=>{ if(!chat_functions.current) return; - if(chat_functions.current.platform === 'AWS') { + const platform = chat_functions.current.platform + if(platform) { (async function() { - if(await setClient(client)) { - await idb.updateOne('chat-history', {client}, [{uid}]) + let set_result = + platform === "AWS" ? await setAwsClient(client) : + platform === "OpenAI" ? await setOpenaiClient(client) : + null; + + if(set_result) { + updateClient(set_result); } })() } // eslint-disable-next-line - }, [chat_functions, client]) + }, [client]) return (
diff --git a/src/components/chat/Tickets.jsx b/src/components/chat/Tickets.jsx index 3265434..d91491b 100644 --- a/src/components/chat/Tickets.jsx +++ b/src/components/chat/Tickets.jsx @@ -1,17 +1,16 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import Ticket from "./Ticket"; import useIDB from "../../utils/idb"; import { genRandomID } from "../../utils/tools"; -export default function Tickets({selectChat, current_chat}) { +export default function Tickets({selectChat, current_chat, history, setHistory}) { - const [tickets, setTickets] = useState([]); const idb = useIDB(); async function syncHistory() { const history = await idb.getAll('chat-history') history.sort((a, b)=>b.updatedAt - a.updatedAt) - setTickets(history) + setHistory(history) } async function startNewConversation() { @@ -27,9 +26,9 @@ export default function Tickets({selectChat, current_chat}) { ) const new_conv_info = await idb.getByID('chat-history', conv_id); new_conv_info && - setTickets([ + setHistory([ new_conv_info, - ...tickets + ...history ]) selectChat(new_conv_info) } @@ -47,7 +46,7 @@ export default function Tickets({selectChat, current_chat}) { >
Start New Chat
- { tickets.map(elem => { + { history.map(elem => { const { title, uid } = elem; return ( e.uid === chat.uid) + ].client = client; + setHistory(history_cp); + + idb.updateOne('chat-history', {client}, [{uid:chat.uid}]) + } return (
- - + +
) } \ No newline at end of file diff --git a/src/components/settings/AwsSettings.jsx b/src/components/settings/AwsSettings.jsx index b0ed73f..aa7de91 100644 --- a/src/components/settings/AwsSettings.jsx +++ b/src/components/settings/AwsSettings.jsx @@ -4,29 +4,16 @@ import SettingSection from "./SettingSection"; import TextComponent from "./components/TextComponent"; import PasswordComponent from "./components/PasswordComponent"; import { getJSONCredentials, storeCredentials } from "../../utils/workers/aws-worker"; -import { getPlatformSettings } from "../../utils/general_settings"; +import { getPlatformSettings, updatePlatformSettings } from "../../utils/general_settings"; -export default function AwsSettings({ trigger, platform_setting, updatePlatformSetting }) { +export default function AwsSettings({ trigger, enabled, updateEnabled }) { - const [ aws_enabled, setAwsEnabled ] = useState(false); const [ aws_region, setAwsRegion ] = useState(''); const [ aws_key_id, setAwsKeyID ] = useState(''); const [ aws_secret_key, setAwsSecretKey ] = useState(''); const [ aws_session_token, setAwsSessionToken ] = useState(''); const [ aws_model_id, setAwsModelID ] = useState(''); - - function setEnabled(is_enabled) { - if(aws_enabled && !is_enabled) { - updatePlatformSetting({ - enabled_platform: null - }) - } else if(!aws_enabled && is_enabled) { - updatePlatformSetting({ - enabled_platform: 'AWS' - }) - } - } - + function saveSettings() { const credentials = { key_id: aws_key_id, secret_key: aws_secret_key @@ -36,9 +23,9 @@ export default function AwsSettings({ trigger, platform_setting, updatePlatformS } storeCredentials( credentials, aws_key_id && aws_secret_key, - platform_setting.enabled_platform === 'AWS' + enabled ) - updatePlatformSetting({ + updatePlatformSettings({ aws_model_id, aws_region }) } @@ -60,10 +47,6 @@ export default function AwsSettings({ trigger, platform_setting, updatePlatformS })() }, []) - useEffect(()=>{ - setAwsEnabled(platform_setting.enabled_platform === 'AWS'); - }, [platform_setting]) - useEffect(()=>{ trigger && saveSettings(); // eslint-disable-next-line @@ -73,37 +56,37 @@ export default function AwsSettings({ trigger, platform_setting, updatePlatformS ) diff --git a/src/components/settings/OpenaiSettings.jsx b/src/components/settings/OpenaiSettings.jsx new file mode 100644 index 0000000..4611506 --- /dev/null +++ b/src/components/settings/OpenaiSettings.jsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import SettingSection from "./SettingSection"; +import TrueFalseComponent from "./components/TrueFalseComponent"; +import TextComponent from "./components/TextComponent"; +import PasswordComponent from "./components/PasswordComponent"; +import { getPlatformSettings, updatePlatformSettings } from "../../utils/general_settings"; +import { getCredentials, storeCredentials } from "../../utils/workers/openai-worker"; + +export default function OpenaiSettings({ trigger, enabled, updateEnabled }) { + + const [api_key, setAPIKey] = useState(''); + const [model_name, setModelName] = useState(''); + + function saveSettings() { + updatePlatformSettings({ + openai_model: model_name + }) + storeCredentials({api_key}) + } + + useEffect(()=>{ + trigger && saveSettings(); + // eslint-disable-next-line + }, [trigger]) + + useEffect(()=>{ + (async function() { + const credentials = await getCredentials(); + if(credentials) { + setAPIKey(credentials.api_key || '') + } + + const { openai_model } = getPlatformSettings(); + setModelName(openai_model); + })() + }, []) + + return ( + + + + + + ) +} \ No newline at end of file diff --git a/src/components/settings/index.jsx b/src/components/settings/index.jsx index 2f3e2fd..7cd84b4 100644 --- a/src/components/settings/index.jsx +++ b/src/components/settings/index.jsx @@ -1,20 +1,17 @@ import { useState } from "react"; import AwsSettings from "./AwsSettings"; -import { getPlatformSettings, updatePlatformSettings as setStorageSetting } from "../../utils/general_settings"; +import { getPlatformSettings, updatePlatformSettings } from "../../utils/general_settings"; import ModelSettings from "./ModelSettings"; +import OpenaiSettings from "./OpenaiSettings"; export default function Settings() { - const [platfom_settings, updatePlatformSettings] = useState(getPlatformSettings()) + const [enabled_platform, setEnabledPlatform] = useState(getPlatformSettings().enabled_platform) const [ saveSettingTrigger, toggleSaveSetting ] = useState(false); - function updateSettings(settings) { - const new_settings = { - ...platfom_settings, - ...settings - } - updatePlatformSettings(new_settings); - setStorageSetting(new_settings); + function updatePlatform(platform = null) { + setEnabledPlatform(platform); + updatePlatformSettings({ enabled_platform: platform }) } function save() { @@ -29,8 +26,13 @@ export default function Settings() { /> updatePlatform(set ? "AWS" : null)} + /> + updatePlatform(set ? "OpenAI" : null)} />
{ saveSettingTrigger ? "Settings Saved!" : "Save Settings" } diff --git a/src/utils/general_settings.js b/src/utils/general_settings.js index 4910a62..b8fc1c5 100644 --- a/src/utils/general_settings.js +++ b/src/utils/general_settings.js @@ -2,7 +2,9 @@ const PLATFORM_SETTINGS_KEY = 'platform-settings' const DEFAULT_PLATFORM_SETTINGS = { enabled_platform: null, // aws - aws_model_id: '', aws_region: '' + aws_model_id: '', aws_region: '', + // openai + openai_model: '' } const MODEL_SETTINGS_KEY = 'general-model-settings' @@ -12,7 +14,7 @@ const DEFAULT_MODEL_SETTINGS = { temperature: 0.7 } -function getSettings(key, default_settings) { +function loadSettings(key, default_settings) { const setting = localStorage.getItem(key); if(!setting) { localStorage.setItem(key, JSON.stringify(default_settings)) @@ -20,22 +22,33 @@ function getSettings(key, default_settings) { return setting ? JSON.parse(setting) : default_settings; } -function updateSettings(key, settings, default_settings) { - localStorage.setItem(key, JSON.stringify({...default_settings, ...settings})); -} +// ============================================================= +// MODEL +// ============================================================= -export function getPlatformSettings() { - return getSettings(PLATFORM_SETTINGS_KEY, DEFAULT_PLATFORM_SETTINGS); +let model_settings = loadSettings(MODEL_SETTINGS_KEY, DEFAULT_MODEL_SETTINGS); + +export function getModelSettings() { + return model_settings; } -export function updatePlatformSettings(settings) { - updateSettings(PLATFORM_SETTINGS_KEY, settings, DEFAULT_PLATFORM_SETTINGS); +export function updateModelSettings(settings) { + model_settings = { ...model_settings, ...settings }; + localStorage.setItem(MODEL_SETTINGS_KEY, JSON.stringify(model_settings)); } -export function getModelSettings() { - return getSettings(MODEL_SETTINGS_KEY, DEFAULT_MODEL_SETTINGS); + +// ============================================================= +// PLATFORM +// ============================================================= + +let platform_settings = loadSettings(PLATFORM_SETTINGS_KEY, DEFAULT_PLATFORM_SETTINGS); + +export function getPlatformSettings() { + return platform_settings; } -export function updateModelSettings(settings) { - updateSettings(MODEL_SETTINGS_KEY, settings, DEFAULT_MODEL_SETTINGS); +export function updatePlatformSettings(settings) { + platform_settings = { ...platform_settings, ...settings } + localStorage.setItem(PLATFORM_SETTINGS_KEY, JSON.stringify(platform_settings)); } \ No newline at end of file diff --git a/src/utils/workers/aws-worker.js b/src/utils/workers/aws-worker.js index 1e9c148..1248e43 100644 --- a/src/utils/workers/aws-worker.js +++ b/src/utils/workers/aws-worker.js @@ -16,10 +16,8 @@ export async function getCredentials(json_credentials = null) { return obj } -export async function storeCredentials(credentials, all_filled, enabled = false) { - const update_result = await instance.updateByID('credentials', 'AWS', {json: credentials}) - if(all_filled && enabled) await initBedrockClient(); - return !!update_result +export async function storeCredentials(credentials) { + return !!(await instance.updateByID('credentials', 'AWS', {json: credentials})) } export async function getJSONCredentials() { @@ -34,7 +32,7 @@ export async function getJSONCredentials() { let bedrock_client = null; export async function setClient(client) { - if(!client) { + if(!client || !(client instanceof BedrockRuntimeClient)) { await initBedrockClient(); return bedrock_client; } else { @@ -153,7 +151,12 @@ export async function chatCompletions(messages, cb = null) { if(system.length) input.system = system; let response_text = '', usage = {} - abort_signal = false; + if(abort_signal) { + abort_signal = false; + cb && cb('', true); + return null; + } + try { const command = new ConverseStreamCommand(input); const response = await bedrock_client.send(command); @@ -168,11 +171,12 @@ export async function chatCompletions(messages, cb = null) { if(abort_signal) break; } cb && cb(response_text, true); - abort_signal = false; } catch(error) { console.error(error); cb && cb(`**${error.name}**:\n\`\`\`\n${error.message}\n\`\`\``, true); return null; + } finally { + abort_signal = false; } return { content: response_text, usage }; diff --git a/src/utils/workers/index.js b/src/utils/workers/index.js index cb32bcb..df30481 100644 --- a/src/utils/workers/index.js +++ b/src/utils/workers/index.js @@ -1,12 +1,13 @@ import { getPlatformSettings } from "../general_settings"; import { chatCompletions as WllamaCompletions, abortCompletion as WllamaAbort } from "./worker"; import { chatCompletions as AwsCompletions, abortCompletion as AwsAbort } from "./aws-worker" +import { chatCompletions as OpenaiCompletions, abortCompletion as OpenaiAbort } from "./openai-worker"; /** * @typedef CompletionFunctions * @property {Function} completions * @property {Function} abort - * @property {"Wllama" | "AWS"} platform + * @property {"Wllama" | "AWS" | "OpenAI"} platform */ /** @@ -19,6 +20,8 @@ export function getCompletionFunctions() { switch(platform_settings.enabled_platform ) { case 'AWS': return { completions: AwsCompletions, abort: AwsAbort, platform: "AWS" } + case 'OpenAI': + return { completions: OpenaiCompletions, abort: OpenaiAbort, platform: "OpenAI"} default: return { completions: WllamaCompletions, abort: WllamaAbort, platform: "Wllama" } } diff --git a/src/utils/workers/openai-worker.js b/src/utils/workers/openai-worker.js new file mode 100644 index 0000000..0eb0c26 --- /dev/null +++ b/src/utils/workers/openai-worker.js @@ -0,0 +1,112 @@ +import OpenAI from 'openai'; +import {instance} from '../idb' +import { getPlatformSettings } from '../general_settings'; + +/** + * @type {OpenAI?} + */ +let openai_client = null; + +let abort_signal = false; + +export async function getCredentials() { + const record = await instance.getByID('credentials', 'OpenAI', ['json']); + if(!record) return null; + return record.json; +} + +export async function storeCredentials(credentials) { + return !!(await instance.updateByID('credentials', 'OpenAI', {json: credentials})) +} + +async function initOpenAIClient() { + const credentials = await getCredentials(); + if(!credentials) return false; + openai_client = new OpenAI({ + apiKey: credentials.api_key, + dangerouslyAllowBrowser: true + }) + return true; +} + +export async function setClient(client) { + if(!client || !(client instanceof OpenAI)) { + await initOpenAIClient(); + return openai_client; + } else { + openai_client = client; + return null; + } +} + +/** + * @typedef Message + * @property {"user"|"assistant"|"system"} role Sender + * @property {String} content Message content + */ + +/** + * @typedef UsageObj + * @property {Number} inputTokens + * @property {Number} outputTokens + * @property {Number} totalTokens + */ + +/** + * @typedef CompletionResponse + * @property { String } content Content of response + * @property { UsageObj } usage + */ + +/** + * @callback CompletionCallback + * @param {String} text Whole text message been generated from start + * @param {Boolean} is_finished Specify is response finished or not + */ + +/** + * Do completion use aws settings + * @param {Message[]} messages Messages you need to send + * @param {CompletionCallback} cb Callback function + * @returns { Promise } + */ +export async function chatCompletions(messages, cb = null) { + if(!openai_client && !(await initOpenAIClient())) return; + + const { openai_model:model } = getPlatformSettings(); + + if(abort_signal) { + abort_signal = false; + cb && cb('', true); + return null; + } + + let response_text = '', usage = {}; + try { + const stream = await openai_client.chat.completions.create({ + model, stream: true, stream_options: { include_usage: true }, + messages + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0].delta + response_text += delta.content || '' + cb && cb(response_text, false); + if(chunk.usage) usage = chunk.usage; + if(chunk.choices[0].finish_reason) break; + if(abort_signal) break; + } + } catch(error) { + console.error(error); + cb && cb(`**${error.name}**:\n\`\`\`\n${error.message}\n\`\`\``, true); + return null; + } finally { + abort_signal = false; + } + + return { content: response_text, usage } +} + +export function abortCompletion() { + abort_signal = true; +} \ No newline at end of file