diff --git a/README.md b/README.md index 9b380e5..9ce420e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Shibuya -[![Lint code](https://github.com/SkywardAI/shibuya/actions/workflows/lint.yml/badge.svg)](https://github.com/SkywardAI/shibuya/actions/workflows/lint.yml) +[![Lint code](https://github.com/SkywardAI/shibuya/actions/workflows/lint.yml/badge.svg)](https://github.com/SkywardAI/shibuya/actions/workflows/lint.yml) [![Release Distribution](https://github.com/SkywardAI/shibuya/actions/workflows/distribution.yml/badge.svg)](https://github.com/SkywardAI/shibuya/actions/workflows/distribution.yml) A project built Electron + React.js, to dig out the potential of cross platform AI completion. ## Development Build @@ -22,6 +22,12 @@ And pnpm run electron ``` One on each terminal, so they won't conflict with each other. + +## Distributions +There are some distribution files in releast page. Please download and run `SkywardaiChat-vX.Y.Z.(AppImage|dmg|exe)` according to your platform. +Currently there's no `Code Signing` in our distributions, so your defender might block you from using the application. Please allow install to use the distributions. + +> Sensitive informations are stored only at your own machine. No one can see them. + ## References * [Wllama](https://github.com/ngxson/wllama) -* [Voy](https://github.com/tantaraio/voy) diff --git a/electron.js b/electron.js index 1386ad1..74f5e2f 100644 --- a/electron.js +++ b/electron.js @@ -12,7 +12,7 @@ function createWindow() { width: 900, minWidth: 560, minHeight: 250, - // autoHideMenuBar: true, + autoHideMenuBar: true, }) if(app.isPackaged) { diff --git a/package.json b/package.json index b86ac0b..bc9e849 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "name": "Bohan Cheng", "email": "cbh778899@outlook.com" }, - "version": "0.1.9", + "version": "0.1.10", "main": "electron.js", "scripts": { "dev": "npm run start & npm run electron", diff --git a/src/components/Entry.jsx b/src/components/Entry.jsx index 6c0a97b..cb6fe06 100644 --- a/src/components/Entry.jsx +++ b/src/components/Entry.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import useIDB from '../utils/idb' import { downloadModel, isModelDownloaded, loadModel } from '../utils/workers/worker' -import { getPlatformSettings } from '../utils/platform_settings'; +import { getPlatformSettings } from '../utils/general_settings'; export default function Entry({complete}) { diff --git a/src/components/chat/Conversation.jsx b/src/components/chat/Conversation.jsx index 96f4b08..bcb30a6 100644 --- a/src/components/chat/Conversation.jsx +++ b/src/components/chat/Conversation.jsx @@ -89,7 +89,8 @@ export default function Conversation({ uid }) { if(upload_file) { const is_img = upload_file.type.startsWith('image') const file_obj = { - content: new Uint8Array(await upload_file.arrayBuffer()) + content: new Uint8Array(await upload_file.arrayBuffer()), + format: upload_file.name.split('.').pop().toLowerCase() } if(!is_img) file_obj.name = upload_file.name; user_message[ diff --git a/src/components/settings/AwsSettings.jsx b/src/components/settings/AwsSettings.jsx index a35e12f..b0ed73f 100644 --- a/src/components/settings/AwsSettings.jsx +++ b/src/components/settings/AwsSettings.jsx @@ -4,13 +4,15 @@ 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/platform_settings"; +import { getPlatformSettings } from "../../utils/general_settings"; -export default function AwsSettings({ platform_setting, updatePlatformSetting }) { +export default function AwsSettings({ trigger, platform_setting, updatePlatformSetting }) { const [ aws_enabled, setAwsEnabled ] = useState(false); const [ aws_region, setAwsRegion ] = useState(''); - const [ aws_pool_id, setAwsPoolId ] = 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) { @@ -26,12 +28,18 @@ export default function AwsSettings({ platform_setting, updatePlatformSetting }) } function saveSettings() { + const credentials = { + key_id: aws_key_id, secret_key: aws_secret_key + } + if(aws_session_token) { + credentials.session_token = aws_session_token + } storeCredentials( - aws_region, aws_pool_id, + credentials, aws_key_id && aws_secret_key, platform_setting.enabled_platform === 'AWS' ) updatePlatformSetting({ - aws_model_id + aws_model_id, aws_region }) } @@ -41,12 +49,14 @@ export default function AwsSettings({ platform_setting, updatePlatformSetting }) const credentials = await getJSONCredentials(); if(credentials) { - setAwsRegion(credentials.region); - setAwsPoolId(credentials.pool_id); + setAwsKeyID(credentials.key_id); + setAwsSecretKey(credentials.secret_key); + setAwsSessionToken(credentials.session_token); } - const { aws_model_id: model_id } = getPlatformSettings(); + const { aws_model_id: model_id, aws_region: region } = getPlatformSettings(); setAwsModelID(model_id); + setAwsRegion(region); })() }, []) @@ -54,22 +64,39 @@ export default function AwsSettings({ platform_setting, updatePlatformSetting }) setAwsEnabled(platform_setting.enabled_platform === 'AWS'); }, [platform_setting]) + useEffect(()=>{ + trigger && saveSettings(); + // eslint-disable-next-line + }, [trigger]) + return ( - + + -
save settings
) } \ No newline at end of file diff --git a/src/components/settings/ModelSettings.jsx b/src/components/settings/ModelSettings.jsx new file mode 100644 index 0000000..8c35c9a --- /dev/null +++ b/src/components/settings/ModelSettings.jsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; +import ScrollBarComponent from "./components/ScrollBarComponent"; +import SettingSection from "./SettingSection"; +import { getModelSettings, updateModelSettings } from "../../utils/general_settings"; + +export default function ModelSettings({ trigger }) { + + const [max_tokens, setMaxTokens] = useState(0); + const [top_p, setTopP] = useState(0); + const [temperature, setTemperature] = useState(0); + + function saveSettings() { + updateModelSettings({ + max_tokens, top_p, temperature + }) + } + + useEffect(()=>{ + trigger && saveSettings(); + // eslint-disable-next-line + }, [trigger]) + + useEffect(()=>{ + const model_settings = getModelSettings(); + setMaxTokens(model_settings.max_tokens); + setTopP(model_settings.top_p); + setTemperature(model_settings.temperature); + }, []) + + return ( + + + + + + ) +} \ No newline at end of file diff --git a/src/components/settings/components/ScrollBarComponent.jsx b/src/components/settings/components/ScrollBarComponent.jsx new file mode 100644 index 0000000..152e2d7 --- /dev/null +++ b/src/components/settings/components/ScrollBarComponent.jsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react" + +export default function ScrollBarComponent({ cb, value, disabled, title, description, min, max, times_10, step }) { + + const [scrollValue, setScrollValue] = useState((times_10 ? 10 : 1) * value); + const [textValue, setTextValue] = useState(value); + + function checkValue(v) { + v = v || +textValue; + return v <= max && v >= min; + } + + function setValue(value, is_scroll = false) { + if(is_scroll) { + setTextValue(times_10 ? value / 10 : value); + setScrollValue(value); + } else { + if(!isNaN(+value)) { + setScrollValue(times_10 ? value * 10 : value); + } + setTextValue(value); + } + } + + useEffect(()=>{ + !isNaN(+textValue) && checkValue() && cb(textValue); + // eslint-disable-next-line + }, [textValue]) + + useEffect(()=>{ + setScrollValue((times_10 ? 10 : 1) * value) + setTextValue(value); + // eslint-disable-next-line + }, [value]) + + + return ( +
+
{title}
+ { description &&
{description}
} +
+ setValue(evt.target.value, true)} + step={step || 1} value={scrollValue} + min={(times_10 ? 10 : 1) * min} max={(times_10 ? 10 : 1) * max} + disabled={disabled} + /> + setValue(evt.target.value, false)} /> +
+
+ ) +} \ No newline at end of file diff --git a/src/components/settings/components/TrueFalseComponent.jsx b/src/components/settings/components/TrueFalseComponent.jsx index 1ff9830..57653d6 100644 --- a/src/components/settings/components/TrueFalseComponent.jsx +++ b/src/components/settings/components/TrueFalseComponent.jsx @@ -3,8 +3,12 @@ export default function TrueFalseComponent({ cb, value, title, description }) {
{title}
{ description &&
{description}
} -
- cb(evt.target.checked)} /> +
+
OFF
+
+ cb(evt.target.checked)} /> +
+
ON
) diff --git a/src/components/settings/index.jsx b/src/components/settings/index.jsx index 73a5b1f..2f3e2fd 100644 --- a/src/components/settings/index.jsx +++ b/src/components/settings/index.jsx @@ -1,10 +1,12 @@ import { useState } from "react"; import AwsSettings from "./AwsSettings"; -import { getPlatformSettings, updatePlatformSettings as setStorageSetting } from "../../utils/platform_settings"; +import { getPlatformSettings, updatePlatformSettings as setStorageSetting } from "../../utils/general_settings"; +import ModelSettings from "./ModelSettings"; export default function Settings() { const [platfom_settings, updatePlatformSettings] = useState(getPlatformSettings()) + const [ saveSettingTrigger, toggleSaveSetting ] = useState(false); function updateSettings(settings) { const new_settings = { @@ -15,12 +17,24 @@ export default function Settings() { setStorageSetting(new_settings); } + function save() { + toggleSaveSetting(true); + setTimeout(()=>toggleSaveSetting(false), 1000); + } + return (
+ +
+ { saveSettingTrigger ? "Settings Saved!" : "Save Settings" } +
) } \ No newline at end of file diff --git a/src/styles/settings.css b/src/styles/settings.css index ec33c1a..4b99b5a 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -6,6 +6,27 @@ overflow: auto; } +.setting-page > .save-settings { + position: fixed; + right: 20px; + top: 20px; + width: fit-content; + height: fit-content; + background-color: dodgerblue; + color: white; + padding: 10px 20px; + user-select: none; + z-index: 10; + border-radius: 10px; + transition-duration: .3s; +} +.setting-page > .save-settings:not(.saved):hover { + background-color: rgb(24, 115, 205); +} +.setting-page > .save-settings.saved { + background-color: limegreen; +} + .setting-page > .setting-section > .title { font-size: 25px; font-weight: bold; @@ -62,9 +83,21 @@ margin: auto; } -.setting-page > .setting-section > .component > .checkbox-container { - --checkbox-width: 100px; +.setting-page > .setting-section > .component > .checkbox { + display: flex; + align-items: center; +} + +.setting-page > .setting-section > .component > .checkbox > .text { + color: gray; + font-weight: bold; +} + +.setting-page > .setting-section > .component > .checkbox > .checkbox-container { + --checkbox-width: 80px; + --checkbox-height: 25px; width: var(--checkbox-width); + height: var(--checkbox-height); background-color: lightgray; border-radius: 50px; background-image: linear-gradient(to right, limegreen 50%, transparent 50%); @@ -72,13 +105,16 @@ background-position: 100%; transition-duration: .3s; border: 1px solid gray; + position: relative; + margin-right: 20px; + margin-left: 20px; } -.setting-page > .setting-section > .component > .checkbox-container::after { +.setting-page > .setting-section > .component > .checkbox > .checkbox-container::after { content: ""; display: block; - width: var(--main-height); - height: var(--main-height); + width: var(--checkbox-height); + height: var(--checkbox-height); border-radius: 50px; position: absolute; left: -2px; @@ -89,7 +125,7 @@ transition-duration: .3s; } -.setting-page > .setting-section > .component > .checkbox-container > input { +.setting-page > .setting-section > .component > .checkbox > .checkbox-container > input { width: 100%; height: 100%; position: absolute; @@ -99,10 +135,31 @@ opacity: 0; } -.setting-page > .setting-section > .component > .checkbox-container:has(input:checked) { +.setting-page > .setting-section > .component > .checkbox > .checkbox-container:has(input:checked) { background-position: 0%; } -.setting-page > .setting-section > .component > .checkbox-container:has(input:checked)::after { - transform: translateX(calc(var(--checkbox-width) - var(--main-height))); +.setting-page > .setting-section > .component > .checkbox > .checkbox-container:has(input:checked)::after { + transform: translateX(calc(var(--checkbox-width) - var(--checkbox-height))); +} + +.setting-page > .setting-section > .component > .scroll-group { + display: flex; + align-items: center; + + --elem-margin: 5px; + --range-width: calc(70% - var(--elem-margin)); + --text-width: calc(30% - var(--elem-margin)); +} + +.setting-page > .setting-section > .component > .scroll-group > input[type='range'] { + width: var(--range-width); + margin-right: var(--elem-margin); + padding: unset; +} +.setting-page > .setting-section > .component > .scroll-group > input[type='text'] { + text-align: center; + height: 100%; + width: var(--text-width); + margin-left: var(--elem-margin); } \ No newline at end of file diff --git a/src/utils/general_settings.js b/src/utils/general_settings.js new file mode 100644 index 0000000..4910a62 --- /dev/null +++ b/src/utils/general_settings.js @@ -0,0 +1,41 @@ +const PLATFORM_SETTINGS_KEY = 'platform-settings' +const DEFAULT_PLATFORM_SETTINGS = { + enabled_platform: null, + // aws + aws_model_id: '', aws_region: '' +} + +const MODEL_SETTINGS_KEY = 'general-model-settings' +const DEFAULT_MODEL_SETTINGS = { + max_tokens: 128, + top_p: 0.9, + temperature: 0.7 +} + +function getSettings(key, default_settings) { + const setting = localStorage.getItem(key); + if(!setting) { + localStorage.setItem(key, JSON.stringify(default_settings)) + } + return setting ? JSON.parse(setting) : default_settings; +} + +function updateSettings(key, settings, default_settings) { + localStorage.setItem(key, JSON.stringify({...default_settings, ...settings})); +} + +export function getPlatformSettings() { + return getSettings(PLATFORM_SETTINGS_KEY, DEFAULT_PLATFORM_SETTINGS); +} + +export function updatePlatformSettings(settings) { + updateSettings(PLATFORM_SETTINGS_KEY, settings, DEFAULT_PLATFORM_SETTINGS); +} + +export function getModelSettings() { + return getSettings(MODEL_SETTINGS_KEY, DEFAULT_MODEL_SETTINGS); +} + +export function updateModelSettings(settings) { + updateSettings(MODEL_SETTINGS_KEY, settings, DEFAULT_MODEL_SETTINGS); +} \ No newline at end of file diff --git a/src/utils/platform_settings.js b/src/utils/platform_settings.js deleted file mode 100644 index 9b39423..0000000 --- a/src/utils/platform_settings.js +++ /dev/null @@ -1,18 +0,0 @@ -const PLATFORM_SETTINGS_KEY = 'platform-settings' -const DEFAULT_PLATFORM_SETTINGS = { - enabled_platform: null, - // aws - aws_model_id: '' -} - -export function getPlatformSettings() { - const setting = localStorage.getItem(PLATFORM_SETTINGS_KEY); - if(!setting) { - localStorage.setItem(PLATFORM_SETTINGS_KEY, JSON.stringify(DEFAULT_PLATFORM_SETTINGS)) - } - return setting ? JSON.parse(setting) : DEFAULT_PLATFORM_SETTINGS; -} - -export function updatePlatformSettings(settings) { - localStorage.setItem(PLATFORM_SETTINGS_KEY, JSON.stringify(settings)) -} \ No newline at end of file diff --git a/src/utils/workers/aws-worker.js b/src/utils/workers/aws-worker.js index 6d1eff7..76657fd 100644 --- a/src/utils/workers/aws-worker.js +++ b/src/utils/workers/aws-worker.js @@ -1,26 +1,24 @@ -import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers"; import { BedrockRuntimeClient, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime"; import { instance } from "../idb"; -import { getPlatformSettings } from "../platform_settings"; -import { genRandomID } from '../tools' +import { getModelSettings, getPlatformSettings } from "../general_settings"; export async function getCredentials(json_credentials = null) { const credentials = json_credentials || (await getJSONCredentials()); if(!credentials) return null; - return fromCognitoIdentityPool({ - clientConfig: { region: credentials.region }, - identityPoolId: credentials.pool_id, - logins: { - 'skywardai-developer-id-provider': genRandomID() - } - }) + const obj = { + accessKeyId: credentials.key_id, + secretAccessKey: credentials.secret_key, + } + if(credentials.session_token) { + obj.sessionToken = credentials.session_token + } + return obj } -export async function storeCredentials(region, pool_id, enabled = false) { - console.log(region, pool_id) - const update_result = await instance.updateByID('credentials', 'AWS', {json: JSON.stringify({region, pool_id})}) - if(region && pool_id && enabled) await initBedrockClient(); +export async function storeCredentials(credentials, all_filled, enabled = false) { + const update_result = await instance.updateByID('credentials', 'AWS', {json: JSON.stringify(credentials)}) + if(all_filled && enabled) await initBedrockClient(); return !!update_result } @@ -38,9 +36,9 @@ export async function initBedrockClient() { const credentials = await getJSONCredentials(); if(!credentials) return false; + const { aws_region: region } = getPlatformSettings(); bedrock_client = new BedrockRuntimeClient({ - region: credentials.region, - credentials: (await getCredentials(credentials)) + region, credentials: (await getCredentials(credentials)) }); return true; } @@ -94,8 +92,8 @@ let abort_signal = false; * @returns { Promise } */ export async function chatCompletions(messages, cb = null) { - const { aws_model_id } = getPlatformSettings(); - if(!aws_model_id || (!bedrock_client && !await initBedrockClient())) { + const { aws_model_id, aws_region } = getPlatformSettings(); + if(!aws_model_id || !aws_region || (!bedrock_client && !await initBedrockClient())) { console.log('no bedrock') cb && cb("**Cannot Initialize AWS Bedrock Client**", true) return null; @@ -109,41 +107,39 @@ export async function chatCompletions(messages, cb = null) { system.push({text: content}); return; } - normal_messages.push({ role, content: [{text: content}] }); + const message = { + role, content: [{text: content}] + } if(image) { - normal_messages.push({ - role, content: [ - { image: { - format: image.format, - source: { bytes: image.content } - } } - ] - }) + message.content.push( + { image: { + format: image.format, + source: { bytes: image.content } + } } + ) } if(document) { - normal_messages.push({ - role, content: [ - { document: { + message.content.push( + { document: { format: document.format, name: document.name, source: { bytes: document.content } - } } - ] - }) + } } + ) } + normal_messages.push(message); }) + const { max_tokens:maxTokens, top_p:topP, temperature } = getModelSettings(); const input = { modelId: aws_model_id, messages: normal_messages, inferenceConfig: { - maxTokens: 128, - temperature: 0.7, - topP: 0.9 + maxTokens, temperature, topP } } - if(system) input.system = system; + if(system.length) input.system = system; let response_text = '', usage = {} abort_signal = false; diff --git a/src/utils/workers/index.js b/src/utils/workers/index.js index f5873c4..cb32bcb 100644 --- a/src/utils/workers/index.js +++ b/src/utils/workers/index.js @@ -1,4 +1,4 @@ -import { getPlatformSettings } from "../platform_settings"; +import { getPlatformSettings } from "../general_settings"; import { chatCompletions as WllamaCompletions, abortCompletion as WllamaAbort } from "./worker"; import { chatCompletions as AwsCompletions, abortCompletion as AwsAbort } from "./aws-worker"