diff --git a/.gitignore b/.gitignore index b737c769..5de9503f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ vite.config.ts.timestamp-* raise.code-workspace recc.md __pycache__/ -.tauri/ \ No newline at end of file +.tauri/ +dist.zip +scripts/ \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 70c1cb47..172cf63d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 2 - versionName "115.0.1" + versionName "122.1.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/release/output-metadata.json b/android/app/release/output-metadata.json index 18c630b0..943cf421 100644 --- a/android/app/release/output-metadata.json +++ b/android/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 2, - "versionName": "115.0.1", + "versionName": "122.1.2", "outputFile": "app-release.apk" } ], diff --git a/package.json b/package.json index c5138501..2d9fe475 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@smithy/protocol-http": "^3.0.12", "@smithy/signature-v4": "^2.0.19", "@tauri-apps/api": "1.5.3", + "@types/markdown-it": "^14.1.1", "@xenova/transformers": "^2.17.1", "blueimp-md5": "^2.19.0", "body-parser": "^1.20.2", @@ -43,6 +44,7 @@ "fflate": "^0.8.1", "gpt-3-encoder": "^1.1.4", "gpt3-tokenizer": "^1.1.5", + "highlight.js": "^11.9.0", "html-to-image": "^1.11.11", "isomorphic-dompurify": "^1.13.0", "jszip": "^3.10.1", @@ -50,10 +52,10 @@ "localforage": "^1.10.0", "lodash": "^4.17.21", "lucide-svelte": "^0.292.0", - "marked": "^5.1.2", + "markdown-it": "^14.1.0", "ml-distance": "^4.0.1", "mobile-drag-drop": "3.0.0-rc.0", - "msgpackr": "^1.10.1", + "msgpackr": "1.10.1", "node-html-parser": "^6.1.12", "ollama": "^0.5.0", "pdfjs-dist": "^4.0.379", @@ -76,7 +78,7 @@ "devDependencies": { "@capacitor/assets": "^3.0.4", "@capacitor/cli": "^5.6.0", - "@sveltejs/vite-plugin-svelte": "^3.0.1", + "@sveltejs/vite-plugin-svelte": "3.0.1", "@swc/core": "1.5.7", "@tailwindcss/typography": "^0.5.10", "@tauri-apps/cli": "1.5.11", @@ -87,7 +89,6 @@ "@types/libsodium-wrappers-sumo": "^0.7.8", "@types/lodash": "^4.14.202", "@types/lodash.isequal": "^4.5.8", - "@types/marked": "^5.0.2", "@types/node": "^18.19.7", "@types/showdown": "^2.0.6", "@types/sortablejs": "^1.15.7", @@ -98,14 +99,14 @@ "autoprefixer": "^10.4.16", "internal-ip": "^7.0.0", "postcss": "^8.4.33", - "svelte": "^4.2.8", - "svelte-check": "^3.6.3", - "svelte-preprocess": "^5.1.3", + "svelte": "4.2.8", + "svelte-check": "3.6.3", + "svelte-preprocess": "5.1.3", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", "typescript": "^5.3.3", - "vite": "^5.2.12", - "vite-plugin-top-level-await": "^1.4.1", - "vite-plugin-wasm": "^3.3.0" + "vite": "5.2.12", + "vite-plugin-top-level-await": "1.4.1", + "vite-plugin-wasm": "3.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e36f08a1..d29ef1e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@tauri-apps/api': specifier: 1.5.3 version: 1.5.3 + '@types/markdown-it': + specifier: ^14.1.1 + version: 14.1.1 '@xenova/transformers': specifier: ^2.17.1 version: 2.17.1 @@ -89,6 +92,9 @@ importers: gpt3-tokenizer: specifier: ^1.1.5 version: 1.1.5 + highlight.js: + specifier: ^11.9.0 + version: 11.9.0 html-to-image: specifier: ^1.11.11 version: 1.11.11 @@ -110,9 +116,9 @@ importers: lucide-svelte: specifier: ^0.292.0 version: 0.292.0(svelte@4.2.8) - marked: - specifier: ^5.1.2 - version: 5.1.2 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 ml-distance: specifier: ^4.0.1 version: 4.0.1 @@ -120,7 +126,7 @@ importers: specifier: 3.0.0-rc.0 version: 3.0.0-rc.0 msgpackr: - specifier: ^1.10.1 + specifier: 1.10.1 version: 1.10.1 node-html-parser: specifier: ^6.1.12 @@ -184,7 +190,7 @@ importers: specifier: ^5.6.0 version: 5.6.0 '@sveltejs/vite-plugin-svelte': - specifier: ^3.0.1 + specifier: 3.0.1 version: 3.0.1(svelte@4.2.8)(vite@5.2.12(@types/node@18.19.7)) '@swc/core': specifier: 1.5.7 @@ -216,9 +222,6 @@ importers: '@types/lodash.isequal': specifier: ^4.5.8 version: 4.5.8 - '@types/marked': - specifier: ^5.0.2 - version: 5.0.2 '@types/node': specifier: ^18.19.7 version: 18.19.7 @@ -250,13 +253,13 @@ importers: specifier: ^8.4.33 version: 8.4.33 svelte: - specifier: ^4.2.8 + specifier: 4.2.8 version: 4.2.8 svelte-check: - specifier: ^3.6.3 + specifier: 3.6.3 version: 3.6.3(postcss-load-config@4.0.2(postcss@8.4.33)(ts-node@10.9.2(@swc/core@1.5.7)(@types/node@18.19.7)(typescript@5.3.3)))(postcss@8.4.33)(svelte@4.2.8) svelte-preprocess: - specifier: ^5.1.3 + specifier: 5.1.3 version: 5.1.3(postcss-load-config@4.0.2(postcss@8.4.33)(ts-node@10.9.2(@swc/core@1.5.7)(@types/node@18.19.7)(typescript@5.3.3)))(postcss@8.4.33)(svelte@4.2.8)(typescript@5.3.3) tailwindcss: specifier: ^3.4.1 @@ -268,13 +271,13 @@ importers: specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.2.12 + specifier: 5.2.12 version: 5.2.12(@types/node@18.19.7) vite-plugin-top-level-await: - specifier: ^1.4.1 + specifier: 1.4.1 version: 1.4.1(rollup@3.29.4)(vite@5.2.12(@types/node@18.19.7)) vite-plugin-wasm: - specifier: ^3.3.0 + specifier: 3.3.0 version: 3.3.0(vite@5.2.12(@types/node@18.19.7)) packages: @@ -1028,6 +1031,9 @@ packages: '@types/libsodium-wrappers@0.7.13': resolution: {integrity: sha512-KeAKtlObirLJk/na6jHBFEdTDjDfFS6Vcr0eG2FjiHKn3Nw8axJFfIu0Y9TpwaauRldQBj/pZm/MHtK76r6OWg==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash.isequal@4.5.8': resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==} @@ -1037,8 +1043,11 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/marked@5.0.2': - resolution: {integrity: sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==} + '@types/markdown-it@14.1.1': + resolution: {integrity: sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -1174,6 +1183,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -2006,6 +2018,10 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2264,6 +2280,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -2332,14 +2351,16 @@ packages: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - marked@5.1.2: - resolution: {integrity: sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==} - engines: {node: '>= 16'} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -2883,6 +2904,10 @@ packages: pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3461,6 +3486,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -4475,6 +4503,8 @@ snapshots: '@types/libsodium-wrappers@0.7.13': {} + '@types/linkify-it@5.0.0': {} + '@types/lodash.isequal@4.5.8': dependencies: '@types/lodash': 4.14.202 @@ -4483,7 +4513,12 @@ snapshots: '@types/long@4.0.2': {} - '@types/marked@5.0.2': {} + '@types/markdown-it@14.1.1': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} '@types/minimist@1.2.5': {} @@ -4612,6 +4647,8 @@ snapshots: arg@5.0.2: {} + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -5571,6 +5608,8 @@ snapshots: he@1.2.0: {} + highlight.js@11.9.0: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -5822,6 +5861,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -5881,10 +5924,19 @@ snapshots: map-obj@4.3.0: {} - marked@5.1.2: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 mdn-data@2.0.30: {} + mdurl@2.0.0: {} + media-typer@0.3.0: {} meow@8.1.2: @@ -6453,6 +6505,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} q@1.5.1: {} @@ -7112,6 +7166,8 @@ snapshots: typescript@5.3.3: {} + uc.micro@2.1.0: {} + uglify-js@3.17.4: optional: true diff --git a/public/lua/json.lua b/public/lua/json.lua new file mode 100644 index 00000000..54d44484 --- /dev/null +++ b/public/lua/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/server.bat b/server.bat index 8003d3b4..0eed9a92 100644 --- a/server.bat +++ b/server.bat @@ -1,3 +1,3 @@ -call npm install -call npm run build -call npm run runserver \ No newline at end of file +call pnpm install +call pnpm run build +call pnpm run runserver diff --git a/server.sh b/server.sh index 5041f2e7..65c62b6d 100644 --- a/server.sh +++ b/server.sh @@ -1,3 +1,3 @@ -npm install -npm run build -npm run runserver \ No newline at end of file +pnpm install +pnpm run build +pnpm run runserver diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 318e3304..30ba0a23 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,204 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64 0.22.1", + "bitflags 2.4.2", + "brotli 6.0.0", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa 1.0.10", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd 0.13.0", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.52", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa 1.0.10", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -28,6 +226,19 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.12", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -130,6 +341,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -186,7 +403,18 @@ checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor", + "brotli-decompressor 2.5.1", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor 4.0.1", ] [[package]] @@ -199,6 +427,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.9.1" @@ -237,6 +475,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + [[package]] name = "bzip2" version = "0.4.4" @@ -425,6 +672,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1650,6 +1908,12 @@ dependencies = [ "selectors", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1688,6 +1952,23 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.11" @@ -1807,6 +2088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2109,6 +2391,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -2556,6 +2844,12 @@ dependencies = [ "regex-syntax 0.8.2", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2653,6 +2947,9 @@ dependencies = [ name = "risuai" version = "0.0.0" dependencies = [ + "actix-cors", + "actix-rt", + "actix-web", "base64 0.21.7", "darling", "eventsource-client", @@ -2663,6 +2960,8 @@ dependencies = [ "tauri", "tauri-build", "tiktoken-rs", + "url", + "uuid", "zip", ] @@ -3010,6 +3309,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3381,7 +3689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" dependencies = [ "base64 0.21.7", - "brotli", + "brotli 3.4.0", "ico", "json-patch", "plist", @@ -3461,7 +3769,7 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21" dependencies = [ - "brotli", + "brotli 3.4.0", "ctor", "dunce", "glob", @@ -3626,7 +3934,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "windows-sys 0.48.0", ] @@ -3755,6 +4065,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3884,9 +4195,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "getrandom 0.2.12", ] @@ -4644,6 +4955,26 @@ dependencies = [ "rustix", ] +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "zip" version = "0.6.6" @@ -4661,7 +4992,7 @@ dependencies = [ "pbkdf2", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -4670,7 +5001,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe 7.0.0", ] [[package]] @@ -4683,6 +5023,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.9+zstd.1.5.5" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 325a06ed..6c680735 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,11 @@ zip = "0.6.6" tar = "0.4.40" eventsource-client = "0.12.2" futures = "0.3.30" +actix-web = "4.0" +actix-cors = "0.6" +actix-rt = "2.5" +url = "2.2" +uuid = { version = "1.9.1", features = [ "v4" ] } [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d708daff..40d8ab74 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,10 +11,18 @@ use serde_json::Value; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use base64::{engine::general_purpose, Engine as _}; use tauri::Manager; +use tauri::State; use std::io::Write; +use std::sync::Mutex; use std::{time::Duration, path::Path}; use serde_json::json; use std::collections::HashMap; +use actix_cors::Cors; +use tauri::api::path::app_data_dir; +use actix_web::{web, HttpRequest, HttpResponse, HttpServer, Responder, App, post}; +use std::fs::File; +struct HttpSecret(Mutex); +struct HttpPort(Mutex); #[tauri::command] async fn native_request(url: String, body: String, header: String, method:String) -> String { @@ -375,10 +383,8 @@ fn run_server_local(){ } - #[tauri::command] async fn streamed_fetch(id:String, url:String, headers: String, body: String, handle: tauri::AppHandle) -> String { - //parse headers let headers_json: Value = match serde_json::from_str(&headers) { Ok(h) => h, @@ -448,8 +454,83 @@ async fn streamed_fetch(id:String, url:String, headers: String, body: String, ha } } +#[tauri::command] +fn get_http_secret(secret_state: State) -> String { + secret_state.0.lock().unwrap().clone() +} + +#[tauri::command] +fn get_http_port(port_state: State) -> u16 { + port_state.0.lock().unwrap().clone() +} + +#[post("/")] +async fn write_binary_file_to_appdata(req: HttpRequest, body: web::Bytes, app_handle: web::Data, secret: web::Data) -> impl Responder { + let query = req.query_string(); + let headers = req.headers(); + let req_secret = headers.get("x-tauri-secret").unwrap().to_str().unwrap(); + if req_secret != *secret.as_ref() { + return HttpResponse::Unauthorized().body("Unauthorized"); + } + let params: std::collections::HashMap<_, _> = url::form_urlencoded::parse(query.as_bytes()).into_owned().collect(); + let app_data_dir = app_data_dir(&app_handle.config()).expect("App dir must be returned by tauri"); + if let Some(file_path) = params.get("path") { + let full_path = app_data_dir.join(file_path); + if let Some(parent) = Path::new(&full_path).parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + return HttpResponse::InternalServerError().body(format!("Failed to create directories: {}", e)); + } + } + + match File::create(&full_path) { + Ok(mut file) => { + if let Err(e) = file.write_all(&body) { + return HttpResponse::InternalServerError().body(format!("Failed to write to file: {}", e)); + } + HttpResponse::Ok().body("File written successfully") + } + Err(e) => HttpResponse::InternalServerError().body(format!("Failed to create file: {}", e)), + } + } else { + HttpResponse::BadRequest().body("Missing file path in query string") + } +} + +async fn run_http_server(handle: tauri::AppHandle, secret: String) { + for port in 5354..65535 { + let handle_copy = handle.clone(); + let secret_copy = secret.clone(); + let res = HttpServer::new(move || { + App::new() + .wrap( + Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600) + ) + .app_data(web::PayloadConfig::new(1024 * 1024 * 1024)) // 1 GB + .app_data(web::Data::new(handle_copy.clone())) + .app_data(web::Data::new(secret_copy.clone())) + .service(write_binary_file_to_appdata) + }) + .bind(("127.0.0.1", port)); + match res { + Ok(server) => { + handle.manage(HttpPort(Mutex::new(port))); + let _ = server.run().await; + break; + } + Err(e) => { + eprintln!("Failed to bind to port {}: {}", port, e); + } + } + } +} + fn main() { tauri::Builder::default() + .manage(HttpSecret(uuid::Uuid::new_v4().to_string().into())) .invoke_handler(tauri::generate_handler![ greet, native_request, @@ -461,8 +542,20 @@ fn main() { post_py_install, run_py_server, install_py_dependencies, - streamed_fetch + streamed_fetch, + get_http_secret, + get_http_port ]) + .setup(|app| { + let handle = app.handle().clone(); + let secret_state: State = app.state(); + let secret = secret_state.0.lock().unwrap().clone(); + std::thread::spawn(move || { + let rt = actix_rt::Runtime::new().unwrap(); + rt.block_on(run_http_server(handle.clone(), secret.clone())); + }); + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } @@ -474,4 +567,4 @@ fn header_map_to_json(header_map: &HeaderMap) -> serde_json::Value { map.insert(key.as_str().to_string(), value.to_str().unwrap().to_string()); } json!(map) -} \ No newline at end of file +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9414d922..0d4f6690 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "RisuAI", - "version": "115.0.1" + "version": "122.1.2" }, "tauri": { "allowlist": { diff --git a/src/App.svelte b/src/App.svelte index c8169651..7332c8ab 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -11,6 +11,8 @@ import Settings from './lib/Setting/Settings.svelte'; import { showRealmInfoStore } from './ts/characterCards'; import RealmFrame from './lib/UI/Realm/RealmFrame.svelte'; + import { AccountWarning } from './ts/storage/accountStorage'; + import AccountWarningComp from './lib/Others/AccountWarningComp.svelte'; let didFirstSetup: boolean = false let gridOpen = false @@ -63,4 +65,7 @@ {#if $ShowRealmFrameStore} {/if} + {#if $AccountWarning} + + {/if} \ No newline at end of file diff --git a/src/lang/en.ts b/src/lang/en.ts index 4f2785bc..6947c95e 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -131,7 +131,8 @@ export const languageEnglish = { defaultVariables: "Here you can define your own default variables. use `=` format, seperated by newline. for example, `name=RisuAI`, which then can be used with trigger scripts and variables CBS like `{{getvar::A}}`, `{{setvar::A::B}}` or `{{? $A + 1}}`. if prompt template's default variable and character's default variable has same name, character's default variable will be used.", lowLevelAccess: "If enabled, it will enable access to features that requires high computing powers and executing AI model via triggers in the character. do not enable this unless you really need these features.", triggerLLMPrompt: "A prompt that would be sent to the model. you can use multi turns and roles by using `@@role user`, `@@role system`, `@@role assistant`. for example, \n\`\`\`\n@@role system\nrespond as hello\n@@role assistant\nhello\n@@role user\nhi\n\`\`\`", - legacyTranslation: "If enabled, it will use the old translation method, which preprocess markdown and quotes before translations instead of postprocessing after translations." + legacyTranslation: "If enabled, it will use the old translation method, which preprocess markdown and quotes before translations instead of postprocessing after translations.", + luaHelp: "You can use Lua scripts as a trigger script. you can define onInput, onOutput, onStart functions. onInput is called when user sends a message, onOutput is called when character sends a message, onStart is called when the chat starts. for more information, see the documentation.", }, setup: { chooseProvider: "Choose AI Provider", @@ -663,4 +664,17 @@ export const languageEnglish = { doNotTranslate: "Do Not Translate", includePersonaName: "Include Persona Name", hidePersonaName: "Hide Persona Name", + triggerSwitchWarn: "If you change the trigger type, current triggers will be lost. do you want to continue?", + codeMode: "Code", + blockMode: "Block", + helpBlock: "Help", + hideChatIcon: "Hide Icon UI", + loadInternalBackup: "Load Internal Backup", + createCopy: "Create a Copy", + bindPersona: "Bind Persona", + chatOptions: "Chat Options", + doYouWantToBindCurrentPersona: "Do you want to bind the current persona to this chat?", + doYouWantToUnbindCurrentPersona: "Do you want to unbind the persona from this chat?", + personaBindedSuccess: "Persona is successfully binded", + personaUnbindedSuccess: "Persona is successfully unbinded", } \ No newline at end of file diff --git a/src/lib/ChatScreens/BackgroundDom.svelte b/src/lib/ChatScreens/BackgroundDom.svelte index 162d22d3..11de4f56 100644 --- a/src/lib/ChatScreens/BackgroundDom.svelte +++ b/src/lib/ChatScreens/BackgroundDom.svelte @@ -1,7 +1,7 @@ -{#if backgroundHTML} - {#key $CurrentVariablePointer} -
- {#await ParseMarkdown(risuChatParser(backgroundHTML, {chara:currentChar}), currentChar, 'back') then md} - {@html md} - {/await} -
- {/key} +{#if backgroundHTML || $moduleBackgroundEmbedding} + {#if $selectedCharID > -1} + {#key $CurrentVariablePointer} + {#key $ReloadGUIPointer} +
+ {#await ParseMarkdown(risuChatParser((backgroundHTML || '') + ($moduleBackgroundEmbedding || ''), {chara:currentChar}), currentChar, 'back') then md} + {@html md} + {/await} +
+ {/key} + {/key} + {/if} {/if} \ No newline at end of file diff --git a/src/lib/ChatScreens/Chat.svelte b/src/lib/ChatScreens/Chat.svelte index c4657710..89f1cb2b 100644 --- a/src/lib/ChatScreens/Chat.svelte +++ b/src/lib/ChatScreens/Chat.svelte @@ -5,7 +5,7 @@ import { alertConfirm, alertError, alertRequestData } from "../../ts/alert"; import { language } from "../../lang"; import { DataBase, type MessageGenerationInfo } from "../../ts/storage/database"; - import { CurrentCharacter, CurrentChat, CurrentVariablePointer } from "../../ts/stores"; + import { CurrentCharacter, CurrentChat, CurrentVariablePointer, HideIconStore, ReloadGUIPointer } from "../../ts/stores"; import { translateHTML } from "../../ts/translator/translator"; import { risuChatParser } from "src/ts/process/scripts"; import { get } from "svelte/store"; @@ -14,6 +14,7 @@ import { getModelShortName } from "src/ts/model/names"; import { capitalize } from "src/ts/util"; import { longpress } from "src/ts/gui/longtouch"; + import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme"; export let message = '' export let name = '' export let largePortrait = false @@ -85,8 +86,8 @@ let lastParsed = '' let lastCharArg:string|simpleCharacterArgument = null let lastChatId = -10 - let blankMessage = (message === '{{none}}' || message === 'blank') && idx === -1 - $: blankMessage = (message === '{{none}}' || message === 'blank') && idx === -1 + let blankMessage = (message === '{{none}}' || message === '{{blank}}' || message === '') && idx === -1 + $: blankMessage = (message === '{{none}}' || message === '{{blank}}' || message === '') && idx === -1 const markParsing = async (data: string, charArg?: string | simpleCharacterArgument, mode?: "normal" | "back", chatID?: number, translateText?:boolean, tries?:number) => { try { if((!isEqual(lastCharArg, charArg)) || (chatID !== lastChatId)){ @@ -106,7 +107,7 @@ const marked = await ParseMarkdown(data, charArg, 'pretranslate', chatID) translating = true console.log(marked) - const translated = postTranslationParse(await translateHTML(marked, false, charArg, chatID)) + const translated = await postTranslationParse(await translateHTML(marked, false, charArg, chatID)) translating = false lastParsed = translated lastCharArg = charArg @@ -143,7 +144,7 @@
- {#if !blankMessage} + {#if !blankMessage && !$HideIconStore} {#if $CurrentCharacter?.chaId === "§playground"}
@@ -178,7 +179,7 @@ $CurrentChat = $CurrentChat }}> - {:else if !blankMessage} + {:else if !blankMessage && !$HideIconStore} {name} {/if}
@@ -242,7 +243,7 @@ {#if MessageGenerationInfo && $DataBase.requestInfoInsideChat}
{:else} - { + { if($DataBase.clickToEdit && idx > -1){ editMode = true } @@ -275,14 +276,15 @@ style:font-size="{0.875 * ($DataBase.zoomsize / 100)}rem" style:line-height="{($DataBase.lineHeight ?? 1.25) * ($DataBase.zoomsize / 100)}rem" > - {#key $CurrentVariablePointer} - {#await markParsing(msgDisplay, character, 'normal', idx, translated)} - {@html lastParsed} - {:then md} - {@html md} - {/await} + {#key $ReloadGUIPointer} + {#key $CurrentVariablePointer} + {#await markParsing(msgDisplay, character, 'normal', idx, translated)} + {@html lastParsed} + {:then md} + {@html md} + {/await} + {/key} {/key} - {/if} diff --git a/src/lib/ChatScreens/DefaultChatScreen.svelte b/src/lib/ChatScreens/DefaultChatScreen.svelte index 1e4207d9..b006f717 100644 --- a/src/lib/ChatScreens/DefaultChatScreen.svelte +++ b/src/lib/ChatScreens/DefaultChatScreen.svelte @@ -2,7 +2,7 @@ import Suggestion from './Suggestion.svelte'; import AdvancedChatEditor from './AdvancedChatEditor.svelte'; import { CameraIcon, DatabaseIcon, DicesIcon, GlobeIcon, ImagePlusIcon, LanguagesIcon, Laugh, MenuIcon, MicOffIcon, PackageIcon, Plus, RefreshCcwIcon, ReplyIcon, Send, StepForwardIcon } from "lucide-svelte"; - import { CurrentCharacter, CurrentChat, CurrentUsername, selectedCharID, CurrentUserIcon, CurrentShowMemoryLimit,CurrentSimpleCharacter, PlaygroundStore } from "../../ts/stores"; + import { CurrentCharacter, CurrentChat, CurrentUsername, selectedCharID, CurrentUserIcon, CurrentShowMemoryLimit,CurrentSimpleCharacter, PlaygroundStore, UserIconProtrait } from "../../ts/stores"; import Chat from "./Chat.svelte"; import { DataBase, type Message, type character, type groupChat } from "../../ts/storage/database"; import { getCharImage } from "../../ts/characters"; @@ -587,7 +587,7 @@ message={chat.data} img={getCharImage($CurrentUserIcon, 'css')} isLastMemory={$CurrentChat.lastMemory === (chat.chatId ?? 'none') && $CurrentShowMemoryLimit} - largePortrait={$DataBase.personas[$DataBase.selectedPersona].largePortrait} + largePortrait={$UserIconProtrait} MessageGenerationInfo={chat.generationInfo} /> {/if} diff --git a/src/lib/ChatScreens/Suggestion.svelte b/src/lib/ChatScreens/Suggestion.svelte index 78868f2c..44be1f81 100644 --- a/src/lib/ChatScreens/Suggestion.svelte +++ b/src/lib/ChatScreens/Suggestion.svelte @@ -7,7 +7,7 @@ import { CopyIcon, LanguagesIcon, RefreshCcwIcon } from "lucide-svelte"; import { alertConfirm } from "src/ts/alert"; import { language } from "src/lang"; - import { replacePlaceholders } from "../../ts/util"; + import { getUserName, replacePlaceholders } from "../../ts/util"; import { onDestroy } from 'svelte'; import { processScript } from "src/ts/process/scripts"; import { get } from "svelte/store"; @@ -71,7 +71,7 @@ } ,{ role: 'user', - content: lastMessages.map(b=>(b.role==='char'? currentChar.name : $DataBase.username)+":"+b.data).reduce((a,b)=>a+','+b) + content: lastMessages.map(b=>(b.role==='char'? currentChar.name : getUserName())+":"+b.data).reduce((a,b)=>a+','+b) } ] diff --git a/src/lib/Others/AccountWarningComp.svelte b/src/lib/Others/AccountWarningComp.svelte new file mode 100644 index 00000000..42019494 --- /dev/null +++ b/src/lib/Others/AccountWarningComp.svelte @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/src/lib/Others/AlertComp.svelte b/src/lib/Others/AlertComp.svelte index 05ba2386..c6eae299 100644 --- a/src/lib/Others/AlertComp.svelte +++ b/src/lib/Others/AlertComp.svelte @@ -18,6 +18,7 @@ import { tokenize } from "src/ts/tokenizer"; import TextAreaInput from "../UI/GUI/TextAreaInput.svelte"; import ModuleChatMenu from "../Setting/Pages/Module/ModuleChatMenu.svelte"; + import { ColorSchemeTypeStore } from "src/ts/gui/colorscheme"; let btn let input = '' let cardExportType = 'realm' @@ -71,7 +72,7 @@

Input

{/if} {#if $alertStore.type === 'markdown'} - + {#await ParseMarkdown($alertStore.msg) then msg} {@html msg} {/await} @@ -82,7 +83,7 @@ - {:else if $alertStore.type !== 'select' && $alertStore.type !== 'requestdata' && $alertStore.type !== 'addchar' && $alertStore.type !== 'hypaV2'} + {:else if $alertStore.type !== 'select' && $alertStore.type !== 'requestdata' && $alertStore.type !== 'addchar' && $alertStore.type !== 'hypaV2' && $alertStore.type !== 'chatOptions'} {$alertStore.msg} {#if $alertStore.submsg} {$alertStore.submsg} @@ -367,6 +368,48 @@
+ {:else if $alertStore.type === 'chatOptions'} +
+

+ {language.chatOptions} +

+ + + +
{/if}
diff --git a/src/lib/Setting/Pages/AdvancedSettings.svelte b/src/lib/Setting/Pages/AdvancedSettings.svelte index 036d9628..189da417 100644 --- a/src/lib/Setting/Pages/AdvancedSettings.svelte +++ b/src/lib/Setting/Pages/AdvancedSettings.svelte @@ -154,7 +154,7 @@ on:click={async () => { alertMd(getRequestLog()) }} - class="drop-shadow-lg p-3 border-borderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> + class="drop-shadow-lg p-3 border-darkborderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> {language.ShowLog} {#if Capacitor.isNativePlatform()} @@ -162,7 +162,7 @@ on:click={async () => { estaStorage = await capStorageInvestigation() }} - class="drop-shadow-lg p-3 border-borderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> + class="drop-shadow-lg p-3 border-darkborderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> Investigate Storage @@ -182,7 +182,7 @@ on:click={async () => { installPython() }} - class="drop-shadow-lg p-3 border-borderc border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> + class="drop-shadow-lg p-3 border-darkbutton border-solid mt-6 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> Test Python {/if} \ No newline at end of file diff --git a/src/lib/Setting/Pages/BotSettings.svelte b/src/lib/Setting/Pages/BotSettings.svelte index 28a4e88c..b25e6f36 100644 --- a/src/lib/Setting/Pages/BotSettings.svelte +++ b/src/lib/Setting/Pages/BotSettings.svelte @@ -31,15 +31,8 @@ mainPrompt: 0, jailbreak: 0, globalNote: 0, - autoSuggest: 0 } - let lasttokens = { - mainPrompt: '', - jailbreak: '', - globalNote: '', - autoSuggest: '' - } export let openPresetList =false export let goPromptTemplate = () => {} @@ -47,7 +40,6 @@ tokens.mainPrompt = await tokenizeAccurate($DataBase.mainPrompt, true) tokens.jailbreak = await tokenizeAccurate($DataBase.jailbreak, true) tokens.globalNote = await tokenizeAccurate($DataBase.globalNote, true) - tokens.autoSuggest = await tokenizeAccurate($DataBase.autoSuggestPrompt, true) } let advancedBotSettings = false @@ -71,12 +63,12 @@
@@ -126,46 +118,38 @@ Google Bearer Token {/if} - + {#if $DataBase.google.projectId !== 'aigoogle'} Google Project ID - + {/if} - - { - if(!v){ - $DataBase.google.projectId = 'aigoogle' - } - else{ - $DataBase.google.projectId = '' - } - }}/> {/if} {#if $DataBase.aiModel.startsWith('novellist') || $DataBase.subModel.startsWith('novellist')} NovelList {language.apiKey} - + {/if} {#if $DataBase.aiModel.startsWith('mancer') || $DataBase.subModel.startsWith('mancer')} Mancer {language.apiKey} - + {/if} {#if $DataBase.aiModel.startsWith('claude-') || $DataBase.subModel.startsWith('claude-')} Claude {language.apiKey} - - {#if $DataBase.useExperimental} - - {/if} + {/if} {#if $DataBase.aiModel.startsWith('mistral') || $DataBase.subModel.startsWith('mistral')} Mistral {language.apiKey} - + +{/if} +{#if $DataBase.aiModel.startsWith('novelai') || $DataBase.subModel.startsWith('novelai')} + NovelAI Bearer Token + {/if} {#if $DataBase.aiModel === 'reverse_proxy' || $DataBase.subModel === 'reverse_proxy'} URL {language.proxyAPIKey} - + {language.proxyRequestModel} None @@ -174,6 +158,7 @@ GPT-4 GPT-4o GPT-4 32k + GPT-4 Turbo GPT-4 Turbo 1106 GPT-4 Turbo 1106 Vision GPT-3.5 0301 @@ -190,7 +175,7 @@ claude-instant-v1.1-100k claude-3-opus-20240229 claude-3-sonnet-20240229 - + claude-3-5-sonnet-20240620 Custom {#if $DataBase.proxyRequestModel === 'custom'} @@ -198,17 +183,14 @@ {:else}
{/if} -
- -
{/if} {#if $DataBase.aiModel.startsWith('risullm')} Risu {language.apiKey} - + {/if} {#if $DataBase.aiModel.startsWith('cohere')} Cohere {language.apiKey} - + {/if} {#if $DataBase.aiModel === 'ollama-hosted'} Ollama URL @@ -219,7 +201,7 @@ {/if} {#if $DataBase.aiModel === 'openrouter' || $DataBase.subModel === 'openrouter'} Openrouter Key - + Openrouter Model {#await openRouterModels()} @@ -260,14 +242,37 @@ {#if $DataBase.aiModel.startsWith('gpt') || $DataBase.subModel.startsWith('gpt') || $DataBase.aiModel.startsWith('instructgpt') || $DataBase.subModel.startsWith('instructgpt')} OpenAI {language.apiKey} - + {/if} -{#if $DataBase.aiModel.startsWith('gpt') || $DataBase.aiModel === 'reverse_proxy' || $DataBase.aiModel === 'openrouter' || $DataBase.aiModel.startsWith('claude-3')} -
+ +
+ {#if $DataBase.aiModel.startsWith('gpt') || $DataBase.aiModel === 'reverse_proxy' || $DataBase.aiModel === 'openrouter' || $DataBase.aiModel.startsWith('claude-3')} -
-{/if} + {/if} + + {#if $DataBase.aiModel.startsWith('palm2') || $DataBase.subModel.startsWith('palm2') || $DataBase.aiModel.startsWith('gemini') || $DataBase.subModel.startsWith('gemini')} + { + if(!v){ + $DataBase.google.projectId = 'aigoogle' + } + else{ + $DataBase.google.projectId = '' + } + }}/> + {/if} + {#if $DataBase.aiModel.startsWith('claude-') || $DataBase.subModel.startsWith('claude-')} + + {/if} + {#if $DataBase.aiModel === 'reverse_proxy' || $DataBase.subModel === 'reverse_proxy'} + + {/if} + {#if $DataBase.aiModel === "novelai" || $DataBase.subModel === "novelai" || $DataBase.aiModel === 'novelai_kayra' || $DataBase.subModel === 'novelai_kayra'} + + + + {/if} +
{#if $DataBase.aiModel === 'custom' || $DataBase.subModel === 'custom'} {language.plugin} @@ -278,19 +283,6 @@ {/each} {/if} -{#if $DataBase.aiModel === "novelai" || $DataBase.subModel === "novelai" || $DataBase.aiModel === 'novelai_kayra' || $DataBase.subModel === 'novelai_kayra'} - - NovelAI Bearer Token - - -
- -
- -
- -
-{/if} {#if $DataBase.aiModel === "kobold" || $DataBase.subModel === "kobold"} Kobold URL @@ -301,7 +293,7 @@ {#if $DataBase.aiModel.startsWith("horde") || $DataBase.subModel.startsWith("horde") } Horde {language.apiKey} - + {/if} {#if $DataBase.aiModel === 'textgen_webui' || $DataBase.subModel === 'textgen_webui' @@ -348,52 +340,39 @@ {language.temperature} {#if $DataBase.aiModel.startsWith("novelai")} - + {:else} - + {/if} -{($DataBase.temperature / 100).toFixed(2)} - {#if $DataBase.aiModel.startsWith('openrouter') || $DataBase.aiModel.startsWith('claude-3') || $DataBase.aiModel.startsWith('cohere-')} Top K - - {($DataBase.top_k).toFixed(0)} + {/if} {#if $DataBase.aiModel.startsWith('openrouter')} Repetition penalty - - {($DataBase.repetition_penalty).toFixed(2)} + Min P - - {($DataBase.min_p).toFixed(2)} + Top A - - {($DataBase.top_a).toFixed(2)} + {/if} {#if $DataBase.aiModel === 'textgen_webui' || $DataBase.aiModel === 'mancer' || $DataBase.aiModel.startsWith('local_') || $DataBase.aiModel.startsWith('hf:::')} Repetition Penalty - - {($DataBase.ooba.repetition_penalty).toFixed(2)} + Length Penalty - - {($DataBase.ooba.length_penalty).toFixed(2)} + Top K - - {($DataBase.ooba.top_k).toFixed(0)} + Top P - - {($DataBase.ooba.top_p).toFixed(2)} + Typical P - - {($DataBase.ooba.typical_p).toFixed(2)} + Top A - - {($DataBase.ooba.top_a).toFixed(2)} + No Repeat n-gram Size - - {($DataBase.ooba.no_repeat_ngram_size).toFixed(0)} +
@@ -444,16 +423,6 @@
- - {language.autoSuggest} - - {tokens.autoSuggest} {language.tokens} - - {language.autoSuggest} Prefix - - - - {:else if $DataBase.aiModel.startsWith('novelai')} @@ -464,91 +433,60 @@ Top P - - {($DataBase.NAIsettings.topP).toFixed(2)} + Top K - - {($DataBase.NAIsettings.topK).toFixed(0)} + Top A - - {($DataBase.NAIsettings.topA).toFixed(2)} + Tailfree Sampling - - {($DataBase.NAIsettings.tailFreeSampling).toFixed(3)} + Typical P - - {($DataBase.NAIsettings.typicalp).toFixed(2)} + Repetition Penalty - - {($DataBase.NAIsettings.repetitionPenalty).toFixed(2)} + Repetition Penalty Range - - {($DataBase.NAIsettings.repetitionPenaltyRange).toFixed(0)} + Repetition Penalty Slope - - {($DataBase.NAIsettings.repetitionPenaltySlope).toFixed(2)} + Frequency Penalty - - {($DataBase.NAIsettings.frequencyPenalty).toFixed(2)} + Presence Penalty - - {($DataBase.NAIsettings.presencePenalty).toFixed(2)} + Mirostat LR - - {($DataBase.NAIsettings.mirostat_lr).toFixed(2)} + Mirostat Tau - - {($DataBase.NAIsettings.mirostat_tau).toFixed(2)} + Cfg Scale - - {($DataBase.NAIsettings.cfg_scale).toFixed(2)} + {:else if $DataBase.aiModel.startsWith('novellist')} Top P - - {($DataBase.ainconfig.top_p).toFixed(2)} + Reputation Penalty - - {($DataBase.ainconfig.rep_pen).toFixed(2)} + Reputation Penalty Range - - {($DataBase.ainconfig.rep_pen_range).toFixed(2)} + Reputation Penalty Slope - - {($DataBase.ainconfig.rep_pen_slope).toFixed(2)} + Top K - - {($DataBase.ainconfig.top_k).toFixed(2)} + Top A - - {($DataBase.ainconfig.top_a).toFixed(2)} + Typical P - - {($DataBase.ainconfig.typical_p).toFixed(2)} + {:else if $DataBase.aiModel.startsWith('claude')} Top P - - {($DataBase.top_p).toFixed(2)} - {language.autoSuggest} - - {tokens.autoSuggest} {language.tokens} + {:else} Top P - - {($DataBase.top_p).toFixed(2)} + {language.frequencyPenalty} - - {($DataBase.frequencyPenalty / 100).toFixed(2)} + {language.presensePenalty} - - {($DataBase.PresensePenalty / 100).toFixed(2)} - - {language.autoSuggest} - - {tokens.autoSuggest} {language.tokens} + {/if} {/if} diff --git a/src/lib/Setting/Pages/Communities.svelte b/src/lib/Setting/Pages/Communities.svelte index eed1e69e..d65eb291 100644 --- a/src/lib/Setting/Pages/Communities.svelte +++ b/src/lib/Setting/Pages/Communities.svelte @@ -6,16 +6,16 @@

{language.community}

\ No newline at end of file diff --git a/src/lib/Setting/Pages/FilesSettings.svelte b/src/lib/Setting/Pages/FilesSettings.svelte index 5a352d9f..10657df6 100644 --- a/src/lib/Setting/Pages/FilesSettings.svelte +++ b/src/lib/Setting/Pages/FilesSettings.svelte @@ -21,7 +21,7 @@ } } }} - class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> + class="drop-shadow-lg p-3 border-darkborderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> {language.savebackup} @@ -37,7 +37,7 @@ } } }} - class="drop-shadow-lg p-3 border-borderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> + class="drop-shadow-lg p-3 border-darkborderc border-solid mt-2 flex justify-center items-center ml-2 mr-2 border-1 hover:bg-selected text-sm"> {language.loadbackup} diff --git a/src/lib/Setting/Pages/Module/ModuleMenu.svelte b/src/lib/Setting/Pages/Module/ModuleMenu.svelte index 091905db..c8641df3 100644 --- a/src/lib/Setting/Pages/Module/ModuleMenu.svelte +++ b/src/lib/Setting/Pages/Module/ModuleMenu.svelte @@ -9,6 +9,10 @@ import TriggerList from "src/lib/SideBars/Scripts/TriggerList.svelte"; import Check from "src/lib/UI/GUI/CheckInput.svelte"; import Help from "src/lib/Others/Help.svelte"; + import TextAreaInput from "src/lib/UI/GUI/TextAreaInput.svelte"; + import Button from "src/lib/UI/GUI/Button.svelte"; + import { openURL } from "src/ts/storage/globalApi"; + import { hubURL } from "src/ts/characterCards"; export let currentModule:RisuModule @@ -59,6 +63,15 @@ } } + function toggleBackground(){ + if(typeof(currentModule.backgroundEmbedding) !== 'string'){ + currentModule.backgroundEmbedding = '' + } + else{ + currentModule.backgroundEmbedding = undefined + } + } + function addLorebook(){ if(Array.isArray(currentModule.lorebook)){ currentModule.lorebook.push({ @@ -117,7 +130,9 @@ - + {#if (Array.isArray(currentModule.lorebook))} @@ -143,15 +158,68 @@ {/if} +{#if typeof(currentModule.backgroundEmbedding) === 'string'} + {language.backgroundHTML} + +{/if} + {#if (Array.isArray(currentModule.trigger))} {language.triggerScript} - - +
+ + +
+ {#if currentModule?.trigger?.[0]?.effect?.[0]?.type === 'triggerlua'} + + + {:else} + + + {/if}
-{/if} \ No newline at end of file +{/if} + +
+ +
\ No newline at end of file diff --git a/src/lib/Setting/Pages/OtherBotSettings.svelte b/src/lib/Setting/Pages/OtherBotSettings.svelte index 5cc25c17..6a9154b3 100644 --- a/src/lib/Setting/Pages/OtherBotSettings.svelte +++ b/src/lib/Setting/Pages/OtherBotSettings.svelte @@ -46,6 +46,7 @@ Novel AI Dall-E Stability API + ComfyUI {#if $DataBase.sdProvider === 'webui'} @@ -243,6 +244,28 @@ {/if} {/if} + + {#if $DataBase.sdProvider === 'comfy'} + The first image generated by the prompt will be selected. + {#if !isTauri} + "Please run comfyUI with --enable-cors-header." + {/if} + ComfyUI {language.providerURL} + + Workflow + + + Positive Text Node: ID + + Positive Text Node: Input Field Name + + Negative Text Node: ID + + Positive Text Node: Input Field Name + + Timeout (sec) + + {/if} @@ -370,4 +393,4 @@ {/if} - \ No newline at end of file + diff --git a/src/lib/Setting/Pages/PersonaSettings.svelte b/src/lib/Setting/Pages/PersonaSettings.svelte index 7e668bbf..53b74173 100644 --- a/src/lib/Setting/Pages/PersonaSettings.svelte +++ b/src/lib/Setting/Pages/PersonaSettings.svelte @@ -5,7 +5,7 @@ import Check from "src/lib/UI/GUI/CheckInput.svelte"; import TextAreaInput from "src/lib/UI/GUI/TextAreaInput.svelte"; import TextInput from "src/lib/UI/GUI/TextInput.svelte"; - import { alertConfirm, alertError, alertSelect } from "src/ts/alert"; + import { alertConfirm, alertSelect } from "src/ts/alert"; import { getCharImage } from "src/ts/characters"; import { changeUserPersona, exportUserPersona, importUserPersona, saveUserPersona, selectUserImg } from "src/ts/persona"; import { DataBase, setDatabase } from "src/ts/storage/database"; @@ -80,7 +80,7 @@
{language.name} - + {language.description} is a 20 year old girl.]`} />
diff --git a/src/lib/Setting/Pages/UserSettings.svelte b/src/lib/Setting/Pages/UserSettings.svelte index b1d4592b..7418e4d5 100644 --- a/src/lib/Setting/Pages/UserSettings.svelte +++ b/src/lib/Setting/Pages/UserSettings.svelte @@ -4,8 +4,8 @@ import { loadRisuAccountBackup, loadRisuAccountData, saveRisuAccountData } from "src/ts/drive/accounter"; import { DataBase } from "src/ts/storage/database"; import Check from "src/lib/UI/GUI/CheckInput.svelte"; - import { alertConfirm, alertNormal } from "src/ts/alert"; - import { forageStorage, isNodeServer, isTauri } from "src/ts/storage/globalApi"; + import { alertConfirm, alertError, alertNormal } from "src/ts/alert"; + import { forageStorage, isNodeServer, isTauri, loadInternalBackup } from "src/ts/storage/globalApi"; import { unMigrationAccount } from "src/ts/storage/accountStorage"; import { checkDriver } from "src/ts/drive/drive"; import { LoadLocalBackup, SaveLocalBackup } from "src/ts/drive/backuplocal"; @@ -61,21 +61,22 @@ {language.loadBackupLocal} - +{/if} + {/if} @@ -580,7 +578,74 @@ }}> {language.triggerScript} - +
+ + + +
+ {#if currentChar.data?.triggerscript?.[0]?.effect?.[0]?.type === 'triggercode'} + + {:else if currentChar.data?.triggerscript?.[0]?.effect?.[0]?.type === 'triggerlua'} + + + {:else} + + {/if}
+
+ +
+
diff --git a/src/lib/SideBars/LoreBook/LoreBookData.svelte b/src/lib/SideBars/LoreBook/LoreBookData.svelte index 1a9101d8..9b279142 100644 --- a/src/lib/SideBars/LoreBook/LoreBookData.svelte +++ b/src/lib/SideBars/LoreBook/LoreBookData.svelte @@ -27,7 +27,7 @@
- - + {#if showUnrec} + + @@ -160,7 +161,7 @@ {/if} diff --git a/src/main.ts b/src/main.ts index f5d95a04..958a7f29 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import "./styles.css"; import "core-js/actual" +import "./ts/storage/database" import App from "./App.svelte"; import { loadData } from "./ts/storage/globalApi"; import { initHotkey } from "./ts/hotkey"; diff --git a/src/styles.css b/src/styles.css index 4f066c50..97e87ca7 100644 --- a/src/styles.css +++ b/src/styles.css @@ -227,4 +227,19 @@ html, body{ ::highlight(deprecated) { color: var(--risu-theme-textcolor2); text-decoration: line-through; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: ""; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: ""; +} + +.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)):not(pre code) { + color: var(--tw-prose-pre-code); + background-color: var(--tw-prose-pre-bg); + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; } \ No newline at end of file diff --git a/src/ts/alert.ts b/src/ts/alert.ts index adfe8047..7d24f0a8 100644 --- a/src/ts/alert.ts +++ b/src/ts/alert.ts @@ -6,7 +6,10 @@ import { Capacitor } from "@capacitor/core" import { DataBase, type MessageGenerationInfo } from "./storage/database" interface alertData{ - type: 'error'| 'normal'|'none'|'ask'|'wait'|'selectChar'|'input'|'toast'|'wait2'|'markdown'|'select'|'login'|'tos'|'cardexport'|'requestdata'|'addchar'|'hypaV2'|'selectModule', + type: 'error'|'normal'|'none'|'ask'|'wait'|'selectChar' + |'input'|'toast'|'wait2'|'markdown'|'select'|'login' + |'tos'|'cardexport'|'requestdata'|'addchar'|'hypaV2'|'selectModule' + |'chatOptions', msg: string, submsg?: string } @@ -94,6 +97,21 @@ export async function alertAddCharacter() { return get(alertStore).msg } +export async function alertChatOptions() { + alertStore.set({ + 'type': 'chatOptions', + 'msg': language.chatOptions + }) + while(true){ + if (get(alertStore).type === 'none'){ + break + } + await sleep(10) + } + + return parseInt(get(alertStore).msg) +} + export async function alertLogin(){ alertStore.set({ 'type': 'login', diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts index a266e171..9584bf97 100644 --- a/src/ts/characterCards.ts +++ b/src/ts/characterCards.ts @@ -693,7 +693,8 @@ async function importCharacterCardSpec(card:CharacterCardV2Risu|CharacterCardV3, imported: true, source: card?.data?.extensions?.risuai?.source ?? [], ccAssets: ccAssets, - lowLevelAccess: risuext?.lowLevelAccess ?? false + lowLevelAccess: risuext?.lowLevelAccess ?? false, + defaultVariables: data?.extensions?.risuai?.defaultVariables ?? '', } if(card.spec === 'chara_card_v3'){ @@ -1051,7 +1052,7 @@ export function createBaseV3(char:character){ type: 'x-risu-asset', uri: asset[1], name: asset[0], - ext: asset[2] || 'unknown' + ext: asset[2] || 'png' }) } } @@ -1062,7 +1063,7 @@ export function createBaseV3(char:character){ type: 'emotion', uri: asset[1], name: asset[0], - ext: 'unknown' + ext: 'png' }) } @@ -1148,7 +1149,8 @@ export function createBaseV3(char:character){ inlayViewScreen: char.inlayViewScreen, newGenData: char.newGenData, vits: {}, - lowLevelAccess: char.lowLevelAccess ?? false + lowLevelAccess: char.lowLevelAccess ?? false, + defaultVariables: char.defaultVariables ?? '', }, depth_prompt: char.depth_prompt }, diff --git a/src/ts/characters.ts b/src/ts/characters.ts index 8c443b64..ac80e335 100644 --- a/src/ts/characters.ts +++ b/src/ts/characters.ts @@ -3,7 +3,7 @@ import { DataBase, saveImage, setDatabase, type character, type Chat, defaultSdD import { alertConfirm, alertError, alertNormal, alertSelect, alertStore, alertWait } from "./alert"; import { language } from "../lang"; import { decode as decodeMsgpack } from "msgpackr"; -import { checkNullish, findCharacterbyId, selectMultipleFile, selectSingleFile, sleep } from "./util"; +import { checkNullish, findCharacterbyId, getUserName, selectMultipleFile, selectSingleFile, sleep } from "./util"; import { v4 as uuidv4 } from 'uuid'; import { selectedCharID } from "./stores"; import { checkCharOrder, downloadFile, getFileSrc } from "./storage/globalApi"; @@ -197,7 +197,7 @@ export async function exportChat(page:number){ let i = 0 for(const v of chat.message){ alertWait(`Translating... ${i++}/${chat.message.length}`) - const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : db.username + const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : getUserName() chatContentHTML += `

${name}

${await htmlChatParse(v.data)}
@@ -268,7 +268,7 @@ export async function exportChat(page:number){ let i = 0 for(const v of chat.message){ alertWait(`Translating... ${i++}/${chat.message.length}`) - const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : db.username + const name = v.saying ? findCharacterbyId(v.saying).name : v.role === 'char' ? char.name : anonymous ? '×××' : getUserName() chatContentHTML += ` ${name} ${await htmlChatParse(v.data)} @@ -309,7 +309,7 @@ export async function exportChat(page:number){ return `--${findCharacterbyId(v.saying).name}\n${v.data}` } else{ - return `--${v.role === 'char' ? char.name : db.username}\n${v.data}` + return `--${v.role === 'char' ? char.name : getUserName()}\n${v.data}` } }).join('\n\n') @@ -409,7 +409,7 @@ export async function importChat(){ function formatTavernChat(chat:string, charName:string){ const db = get(DataBase) - return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, db.username).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName) + return chat.replace(/<([Uu]ser)>|\{\{([Uu]ser)\}\}/g, getUserName()).replace(/((\{\{)|<)([Cc]har)(=.+)?((\}\})|>)/g, charName) } export function characterFormatUpdate(index:number|character){ diff --git a/src/ts/drive/drive.ts b/src/ts/drive/drive.ts index 7fc17c37..0aa4ee39 100644 --- a/src/ts/drive/drive.ts +++ b/src/ts/drive/drive.ts @@ -95,9 +95,12 @@ export async function checkDriverInit() { return false } } catch (error) { - location.search = '' console.error(error) alertError(`Backup Error: ${error}`) + const currentURL = new URL(location.href) + currentURL.search = '' + window.history.replaceState( {} , "", currentURL.href ); + await sleep(100000) return false } } diff --git a/src/ts/gui/colorscheme.ts b/src/ts/gui/colorscheme.ts index 25e4da65..fdafea1c 100644 --- a/src/ts/gui/colorscheme.ts +++ b/src/ts/gui/colorscheme.ts @@ -1,4 +1,4 @@ -import { get } from "svelte/store"; +import { get, writable } from "svelte/store"; import { DataBase, setDatabase } from "../storage/database"; import { downloadFile } from "../storage/globalApi"; import { BufferToText, selectSingleFile } from "../util"; @@ -96,6 +96,8 @@ const colorShemes = { } as const +export const ColorSchemeTypeStore = writable('dark' as 'dark'|'light') + export const colorSchemeList = Object.keys(colorShemes) as (keyof typeof colorShemes)[] export function changeColorScheme(colorScheme: string){ @@ -114,7 +116,7 @@ export function updateColorScheme(){ let colorScheme = db.colorScheme if(colorScheme == null){ - colorScheme = defaultColorScheme + colorScheme = structuredClone(defaultColorScheme) } //set css variables @@ -127,6 +129,7 @@ export function updateColorScheme(){ document.documentElement.style.setProperty("--risu-theme-textcolor2", colorScheme.textcolor2); document.documentElement.style.setProperty("--risu-theme-darkborderc", colorScheme.darkBorderc); document.documentElement.style.setProperty("--risu-theme-darkbutton", colorScheme.darkbutton); + ColorSchemeTypeStore.set(colorScheme.type) } export function exportColorScheme(){ diff --git a/src/ts/model/names.ts b/src/ts/model/names.ts index c5c9bbbb..45c44c42 100644 --- a/src/ts/model/names.ts +++ b/src/ts/model/names.ts @@ -79,6 +79,8 @@ export function getModelName(name:string){ return 'Gemini Ultra Vision' case 'claude-3-opus-20240229': return 'Claude 3 Opus (20240229)' + case 'claude-3-5-sonnet-20240620': + return 'Claude 3.5 Sonnet (20240620)' case 'claude-3-sonnet-20240229': return 'Claude 3 Sonnet (20240229)' case 'mistral-large-latest': diff --git a/src/ts/mutex.ts b/src/ts/mutex.ts new file mode 100644 index 00000000..8b5e156c --- /dev/null +++ b/src/ts/mutex.ts @@ -0,0 +1,88 @@ +/** + * A lock for synchronizing async operations. + * Use this to protect a critical section + * from getting modified by multiple async operations + * at the same time. + */ +export class Mutex { + /** + * When multiple operations attempt to acquire the lock, + * this queue remembers the order of operations. + */ + private _queue: { + resolve: (release: ReleaseFunction) => void + }[] = [] + + private _isLocked = false + + /** + * Wait until the lock is acquired. + * @returns A function that releases the acquired lock. + */ + acquire() { + return new Promise((resolve) => { + this._queue.push({resolve}) + this._dispatch() + }); + } + + /** + * Enqueue a function to be run serially. + * + * This ensures no other functions will start running + * until `callback` finishes running. + * @param callback Function to be run exclusively. + * @returns The return value of `callback`. + */ + async runExclusive(callback: () => Promise) { + const release = await this.acquire() + try { + return await callback() + } finally { + release() + } + } + + /** + * Check the availability of the resource + * and provide access to the next operation in the queue. + * + * _dispatch is called whenever availability changes, + * such as after lock acquire request or lock release. + */ + private _dispatch() { + if (this._isLocked) { + // The resource is still locked. + // Wait until next time. + return + } + const nextEntry = this._queue.shift() + if (!nextEntry) { + // There is nothing in the queue. + // Do nothing until next dispatch. + return + } + // The resource is available. + this._isLocked = true + // and give access to the next operation + // in the queue. + nextEntry.resolve(this._buildRelease()) + } + + /** + * Build a release function for each operation + * so that it can release the lock after + * the operation is complete. + */ + private _buildRelease(): ReleaseFunction { + return () => { + // Each release function make + // the resource available again + this._isLocked = false + // and call dispatch. + this._dispatch() + } + } +} + +type ReleaseFunction = () => void \ No newline at end of file diff --git a/src/ts/parser.ts b/src/ts/parser.ts index 1c1edfb7..3d4ff799 100644 --- a/src/ts/parser.ts +++ b/src/ts/parser.ts @@ -1,38 +1,44 @@ import DOMPurify from 'isomorphic-dompurify'; -import { Marked } from 'marked'; - -import { DataBase, setDatabase, type Database, type Message, type character, type customscript, type groupChat } from './storage/database'; +import markdownit from 'markdown-it' +import { DataBase, setDatabase, type Database, type Message, type character, type customscript, type groupChat, type triggerscript } from './storage/database'; import { getFileSrc } from './storage/globalApi'; import { processScriptFull } from './process/scripts'; import { get } from 'svelte/store'; import css from '@adobe/css-tools' -import { CurrentChat, SizeStore, selectedCharID } from './stores'; +import { CurrentCharacter, CurrentChat, SizeStore, selectedCharID } from './stores'; import { calcString } from './process/infunctions'; -import { findCharacterbyId, parseKeyValue, sfc32, sleep, uuidtoNumber } from './util'; -import { getInlayImage } from './process/files/image'; -import { risuFormater } from './plugins/automark'; +import { findCharacterbyId, getPersonaPrompt, getUserIcon, getUserName, parseKeyValue, sfc32, sleep, uuidtoNumber } from './util'; +import { getInlayImage, writeInlayImage } from './process/files/image'; import { getModuleLorebooks } from './process/modules'; +import { HypaProcesser } from './process/memory/hypamemory'; +import { generateAIImage } from './process/stableDiff'; +import { requestChatData } from './process/request'; +import type { OpenAIChat } from './process'; +import { alertInput, alertNormal } from './alert'; +import hljs from 'highlight.js/lib/core' +import 'highlight.js/styles/atom-one-dark.min.css' -const mconverted = new Marked({ - gfm: true, +const markdownItOptions = { + html: true, breaks: true, - silent: true, - tokenizer: { - del(src) { - const cap = /^~~~(?=\S)([\s\S]*?\S)~~~/.exec(src); - if (cap) { - return { - type: 'del', - raw: cap[0], - text: cap[1], - tokens: this.lexer.inlineTokens(cap[1]) - }; - } + linkify: false, + typographer: true, + quotes: '\u{E9b0}\u{E9b1}\u{E9b2}\u{E9b3}', //placeholder characters to convert to real quotes +} + +const md = markdownit(markdownItOptions) +const mdHighlight = markdownit({ + highlight: function (str, lang) { + if(lang){ + return ``+ str +''; } - } + return '' + }, + ...markdownItOptions }) - +md.disable(['code']) +mdHighlight.disable(['code']) DOMPurify.addHook("uponSanitizeElement", (node: HTMLElement, data) => { if (data.tagName === "iframe") { @@ -52,6 +58,9 @@ DOMPurify.addHook("uponSanitizeAttribute", (node, data) => { case 'class':{ if(data.attrValue){ data.attrValue = data.attrValue.split(' ').map((v) => { + if(v.startsWith('hljs')){ + return v + } return "x-risu-" + v }).join(' ') } @@ -69,79 +78,265 @@ DOMPurify.addHook("uponSanitizeAttribute", (node, data) => { }) -export const assetRegex = /{{(raw|img|video|audio|bg|emotion|asset|video-img)::(.+?)}}/g +function renderMarkdown(md:markdownit, data:string){ + return md.render(data.replace(/“|”/g, '"').replace(/‘|’/g, "'")) + .replace(/\uE9b0/gu, '“') + .replace(/\uE9b1/gu, '”') + .replace(/\uE9b2/gu, '‘') + .replace(/\uE9b3/gu, '’') +} + +async function renderHighlightableMarkdown(data:string) { + let rendered = renderMarkdown(mdHighlight, data) + const highlightPlaceholders = rendered.match(/(.+?)<\/pre-hljs-placeholder>/gms) + if (!highlightPlaceholders){ + return rendered + } + + for (const placeholder of highlightPlaceholders){ + try { + let lang = placeholder.match(/lang="(.+?)"/)?.[1] + const code = placeholder.match(/(.+?)<\/pre-hljs-placeholder>/ms)?.[1] + if (!lang || !code){ + continue + } + //import language if not already loaded + //we do not refactor this to a function because we want to keep vite to only import the languages that are needed + let languageModule:any = null + switch(lang){ + case 'js': + case 'javascript':{ + lang = 'javascript' + if(!hljs.getLanguage('javascript')){ + languageModule = await import('highlight.js/lib/languages/javascript') + } + break + } + case 'py': + case 'python':{ + lang = 'python' + if(!hljs.getLanguage('python')){ + languageModule = await import('highlight.js/lib/languages/python') + } + break + } + case 'css':{ + lang = 'css' + if(!hljs.getLanguage('css')){ + languageModule = await import('highlight.js/lib/languages/css') + } + break + } + case 'xml': + case 'html':{ + lang = 'xml' + if(!hljs.getLanguage('xml')){ + languageModule = await import('highlight.js/lib/languages/xml') + } + break + } + case 'lua':{ + lang = 'lua' + if(!hljs.getLanguage('lua')){ + languageModule = await import('highlight.js/lib/languages/lua') + } + break + } + case 'dart':{ + lang = 'dart' + if(!hljs.getLanguage('dart')){ + languageModule = await import('highlight.js/lib/languages/dart') + } + break + } + case 'java':{ + lang = 'java' + if(!hljs.getLanguage('java')){ + languageModule = await import('highlight.js/lib/languages/java') + } + break + } + case 'rust':{ + lang = 'rust' + if(!hljs.getLanguage('rust')){ + languageModule = await import('highlight.js/lib/languages/rust') + } + break + } + case 'c': + case 'cpp':{ + lang = 'cpp' + if(!hljs.getLanguage('cpp')){ + languageModule = await import('highlight.js/lib/languages/cpp') + } + break + } + case 'csharp': + case 'cs':{ + lang = 'csharp' + if(!hljs.getLanguage('csharp')){ + languageModule = await import('highlight.js/lib/languages/csharp') + } + break + } + case 'ts': + case 'typescript':{ + lang = 'typescript' + if(!hljs.getLanguage('typescript')){ + languageModule = await import('highlight.js/lib/languages/typescript') + } + break + } + case 'json':{ + lang = 'json' + if(!hljs.getLanguage('json')){ + languageModule = await import('highlight.js/lib/languages/json') + } + break + } + case 'yaml':{ + lang = 'yaml' + if(!hljs.getLanguage('yaml')){ + languageModule = await import('highlight.js/lib/languages/yaml') + } + break + } + case 'shell':{ + lang = 'shell' + if(!hljs.getLanguage('shell')){ + languageModule = await import('highlight.js/lib/languages/shell') + } + break + } + case 'bash':{ + lang = 'bash' + if(!hljs.getLanguage('bash')){ + languageModule = await import('highlight.js/lib/languages/bash') + } + break + } + default:{ + lang = 'none' + } + } + if(languageModule){ + hljs.registerLanguage(lang, languageModule.default) + } + if(lang === 'none'){ + rendered = rendered.replace(placeholder, `
${md.utils.escapeHtml(code)}
`) + } + else{ + const highlighted = hljs.highlight(code, { + language: lang, + ignoreIllegals: true + }).value + rendered = rendered.replace(placeholder, `
${highlighted}
`) + } + } catch (error) { + + } + } + + return rendered + +} + + +export const assetRegex = /{{(raw|img|video|audio|bg|emotion|asset|video-img|source)::(.+?)}}/g async function parseAdditionalAssets(data:string, char:simpleCharacterArgument|character, mode:'normal'|'back', mode2:'unset'|'pre'|'post' = 'unset'){ const db = get(DataBase) const assetWidthString = (db.assetWidth && db.assetWidth !== -1 || db.assetWidth === 0) ? `max-width:${db.assetWidth}rem;` : '' - if(char.additionalAssets || char.emotionImages){ + let assetPaths:{[key:string]:{ + path:string + ext?:string + }} = {} + let emoPaths:{[key:string]:{ + path:string + }} = {} - let assetPaths:{[key:string]:{ - path:string - ext?:string - }} = {} - let emoPaths:{[key:string]:{ - path:string - }} = {} - - if(char.additionalAssets){ - for(const asset of char.additionalAssets){ - const assetPath = await getFileSrc(asset[1]) - assetPaths[asset[0].toLocaleLowerCase()] = { - path: assetPath, - ext: asset[2] - } + if(char.additionalAssets){ + for(const asset of char.additionalAssets){ + const assetPath = await getFileSrc(asset[1]) + assetPaths[asset[0].toLocaleLowerCase()] = { + path: assetPath, + ext: asset[2] } } - if(char.emotionImages){ - for(const emo of char.emotionImages){ - const emoPath = await getFileSrc(emo[1]) - emoPaths[emo[0].toLocaleLowerCase()] = { - path: emoPath, - } + } + if(char.emotionImages){ + for(const emo of char.emotionImages){ + const emoPath = await getFileSrc(emo[1]) + emoPaths[emo[0].toLocaleLowerCase()] = { + path: emoPath, } } - const videoExtention = ['mp4', 'webm', 'avi', 'm4p', 'm4v'] - data = data.replaceAll(assetRegex, (full:string, type:string, name:string) => { - name = name.toLocaleLowerCase() - if(type === 'emotion'){ - const path = emoPaths[name]?.path - if(!path){ - return '' - } - return `${path}` - } - const path = assetPaths[name] + } + const videoExtention = ['mp4', 'webm', 'avi', 'm4p', 'm4v'] + let needsSourceAccess = false + data = data.replaceAll(assetRegex, (full:string, type:string, name:string) => { + name = name.toLocaleLowerCase() + if(type === 'emotion'){ + const path = emoPaths[name]?.path if(!path){ return '' } - switch(type){ - case 'raw': - return path.path - case 'img': - return `${path.path}` - case 'video': - return `` - case 'video-img': - return `` - case 'audio': - return `` - case 'bg': - if(mode === 'back'){ - return `
` - } - break - case 'asset':{ - if(path.ext && videoExtention.includes(path.ext)){ - return `` - } - return `${path.path}` + return `${path}` + } + if(type === 'source'){ + needsSourceAccess = true + switch(name){ + case 'char':{ + return '\uE9b4CHAR\uE9b4' + } + case 'user': { + return '\uE9b4USER\uE9b4' } } + } + const path = assetPaths[name] + if(!path){ return '' - }) + } + switch(type){ + case 'raw': + return path.path + case 'img': + return `${path.path}` + case 'video': + return `` + case 'video-img': + return `` + case 'audio': + return `` + case 'bg': + if(mode === 'back'){ + return `
` + } + break + case 'asset':{ + if(path.ext && videoExtention.includes(path.ext)){ + return `` + } + return `${path.path}` + } + } + return '' + }) + + if(needsSourceAccess){ + const chara = get(CurrentCharacter) + if(chara.image){} + data = data.replace(/\uE9b4CHAR\uE9b4/g, + chara.image ? (await getFileSrc(chara.image)) : '' + ) + + data = data.replace(/\uE9b4USER\uE9b4/g, + getUserIcon() ? (await getFileSrc(getUserIcon())) : '' + ) } + return data } @@ -166,6 +361,7 @@ export interface simpleCharacterArgument{ chaId: string, virtualscript?: string emotionImages?: [string, string][] + triggerscript?: triggerscript[] } @@ -187,8 +383,7 @@ export async function ParseMarkdown(data:string, charArg:(character|simpleCharac data = encodeStyle(data) if(mode === 'normal'){ - data = risuFormater(data) - data = mconverted.parse(data) + data = await renderHighlightableMarkdown(data) } return decodeStyle(DOMPurify.sanitize(data, { ADD_TAGS: ["iframe", "style", "risu-style", "x-em"], @@ -196,8 +391,8 @@ export async function ParseMarkdown(data:string, charArg:(character|simpleCharac })) } -export function postTranslationParse(data:string){ - let lines = risuFormater(data).split('\n') +export async function postTranslationParse(data:string){ + let lines = data.split('\n') for(let i=0;i { +function basicMatcher (p1:string,matcherArg:matcherArg,vars:{[key:string]:string}|null = null ):{ + text:string, + var:{[key:string]:string} +}|string|null { try { if(p1.length > 100000){ return '' @@ -411,19 +610,16 @@ const matcher = (p1:string,matcherArg:matcherArg) => { switch(lowerCased){ case 'previous_char_chat': case 'lastcharmessage':{ - if(chatID !== -1){ - const selchar = db.characters[get(selectedCharID)] - const chat = selchar.chats[selchar.chatPage] - let pointer = chatID - 1 - while(pointer >= 0){ - if(chat.message[pointer].role === 'char'){ - return chat.message[pointer].data - } - pointer-- + const selchar = db.characters[get(selectedCharID)] + const chat = selchar.chats[selchar.chatPage] + let pointer = chatID !== -1 ? chatID - 1 : chat.message.length - 1 + while(pointer >= 0){ + if(chat.message[pointer].role === 'char'){ + return chat.message[pointer].data } - return selchar.firstMsgIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[selchar.firstMsgIndex] + pointer-- } - return '' + return selchar.firstMsgIndex === -1 ? selchar.firstMessage : selchar.alternateGreetings[selchar.firstMsgIndex] } case 'previous_user_chat': case 'lastusermessage':{ @@ -465,7 +661,7 @@ const matcher = (p1:string,matcherArg:matcherArg) => { if(matcherArg.consistantChar){ return 'username' } - return db.username + return getUserName() } case 'personality': case 'char_persona':{ @@ -504,7 +700,7 @@ const matcher = (p1:string,matcherArg:matcherArg) => { } case 'persona': case 'user_persona':{ - return db.personaPrompt + return getPersonaPrompt() } case 'main_prompt': case 'system_prompt':{ @@ -568,7 +764,7 @@ const matcher = (p1:string,matcherArg:matcherArg) => { } case 'first_msg_index':{ const selchar = db.characters[get(selectedCharID)] - return selchar.firstMsgIndex + return selchar.firstMsgIndex.toString() } case 'blank': case 'none':{ @@ -730,6 +926,10 @@ const matcher = (p1:string,matcherArg:matcherArg) => { return db.subModel } case 'role': { + if (chatID !== -1) { + const selchar = db.characters[get(selectedCharID)] + return selchar.chats[selchar.chatPage].message[chatID].role; + } return matcherArg.role ?? 'role' } case 'jbtoggled':{ @@ -756,7 +956,7 @@ const matcher = (p1:string,matcherArg:matcherArg) => { return '' } const chat = selchar.chats[selchar.chatPage] - return chat.message.length - 1 + return (chat.message.length - 1).toString() } case 'emotionlist':{ const selchar = db.characters[get(selectedCharID)] @@ -790,6 +990,28 @@ const matcher = (p1:string,matcherArg:matcherArg) => { if(arra.length > 1){ const v = arra[1] switch(arra[0]){ + case 'tempvar': + case 'gettempvar':{ + return { + text: vars[arra[1]] ?? '', + var: vars + } + } + case 'settempvar':{ + vars[arra[1]] = arra[2] + return { + text: '', + var: vars + } + } + case 'return':{ + vars['__return__'] = arra[1] + vars['__force_return__'] = '1' + return { + text: '', + var: vars + } + } case 'getvar':{ return getChatVar(v) } @@ -935,9 +1157,9 @@ const matcher = (p1:string,matcherArg:matcherArg) => { } case 'tonumber':{ - return makeArray(arra[1].split('').filter((v) => { + return (arra[1].split('').filter((v) => { return !isNaN(Number(v)) || v === '.' - })) + })).join('') } case 'pow':{ return Math.pow(Number(arra[1]), Number(arra[2])).toString() @@ -1106,6 +1328,78 @@ const matcher = (p1:string,matcherArg:matcherArg) => { } })) } + case 'all':{ + const array = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + const all = array.every((f) => { + return f === '1' + }) + return all ? '1' : '0' + } + case 'any':{ + const array = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + const any = array.some((f) => { + return f === '1' + }) + return any ? '1' : '0' + } + case 'min':{ + const val = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + return Math.min(...val.map((f) => { + const num = Number(f) + if(isNaN(num)){ + return 0 + } + return num + })).toString() + } + case 'max':{ + const val = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + return Math.max(...val.map((f) => { + const num = Number(f) + if(isNaN(num)){ + return 0 + } + return num + })).toString() + } + case 'sum':{ + const val = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + return val.map((f) => { + const num = Number(f) + if(isNaN(num)){ + return 0 + } + return num + }).reduce((a, b) => a + b, 0).toString() + } + case 'average':{ + const val = arra.length > 2 ? arra.slice(1) : parseArray(arra[1]) + const sum = val.map((f) => { + const num = Number(f) + if(isNaN(num)){ + return 0 + } + return num + }).reduce((a, b) => a + b, 0) + return (sum / val.length).toString() + } + case 'fixnum': + case 'fix_num': + case 'fixnumber': + case 'fix_number':{ + return Number(arra[1]).toFixed(Number(arra[2])) + } + case 'unicode_encode': + case 'unicodeencode':{ + return arra[1].charCodeAt(arra[2] ? Number(arra[2]) : 0).toString() + } + case 'unicode_decode': + case 'unicodedecode':{ + return String.fromCharCode(Number(arra[1])) + } + case 'hash':{ + return ((pickHashRand(0, arra[1]) * 10000000) + 1).toFixed(0).padStart(7, '0') + } } } if(p1.startsWith('random')){ @@ -1228,17 +1522,20 @@ const dateTimeFormat = (main:string, time = 0) => { return main .replace(/YYYY/g, date.getFullYear().toString()) .replace(/YY/g, date.getFullYear().toString().substring(2)) - .replace(/MM?/g, (date.getMonth() + 1).toString().padStart(2, '0')) - .replace(/DD?/g, date.getDate().toString().padStart(2, '0')) - .replace(/DDDD?/g, (date.getDay() + (date.getMonth() * 30)).toString()) - .replace(/HH?/g, date.getHours().toString().padStart(2, '0')) - .replace(/hh?/g, (date.getHours() % 12).toString().padStart(2, '0')) - .replace(/mm?/g, date.getMinutes().toString().padStart(2, '0')) - .replace(/ss?/g, date.getSeconds().toString().padStart(2, '0')) - .replace(/X?/g, (Date.now() / 1000).toFixed(2)) - .replace(/x?/g, Date.now().toFixed()) + .replace(/MMMM/g, Intl.DateTimeFormat('en', { month: 'long' }).format(date)) + .replace(/MMM/g, Intl.DateTimeFormat('en', { month: 'short' }).format(date)) + .replace(/MM/g, (date.getMonth() + 1).toString().padStart(2, '0')) + .replace(/DDDD/g, Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / (1000 * 60 * 60 * 24)).toString()) + .replace(/DD/g, date.getDate().toString().padStart(2, '0')) + .replace(/dddd/g, Intl.DateTimeFormat('en', { weekday: 'long' }).format(date)) + .replace(/ddd/g, Intl.DateTimeFormat('en', { weekday: 'short' }).format(date)) + .replace(/HH/g, date.getHours().toString().padStart(2, '0')) + .replace(/hh/g, (date.getHours() % 12 || 12).toString().padStart(2, '0')) + .replace(/mm/g, date.getMinutes().toString().padStart(2, '0')) + .replace(/ss/g, date.getSeconds().toString().padStart(2, '0')) + .replace(/X/g, Math.floor(date.getTime() / 1000).toString()) + .replace(/x/g, date.getTime().toString()) .replace(/A/g, date.getHours() >= 12 ? 'PM' : 'AM') - .replace(/MMMM?/g, Intl.DateTimeFormat('en', { month: 'long' }).format(date)) } @@ -1274,7 +1571,7 @@ const smMatcher = (p1:string,matcherArg:matcherArg) => { if(matcherArg.consistantChar){ return 'username' } - return db.username + return getUserName() } } } @@ -1326,7 +1623,12 @@ function parseDict(p1:string):{[key:string]:string}{ } function makeArray(p1:string[]):string{ - return JSON.stringify(p1) + return JSON.stringify(p1.map((f) => { + if(typeof(f) === 'string'){ + return f.replace(/::/g, '\\u003A\\u003A') + } + return f + })) } function blockStartMatcher(p1:string,matcherArg:matcherArg):{type:blockMatch,type2?:string,funcArg?:string[]}{ @@ -1425,6 +1727,8 @@ export function risuChatParser(da:string, arg:{ let commentLatest:string[] = [""] let commentV = new Uint8Array(512) let thinkingMode = false + let tempVar:{[key:string]:string} = {} + const matcherObj = { chatID: chatID, chara: chara, @@ -1435,7 +1739,7 @@ export function risuChatParser(da:string, arg:{ displaying: arg.visualize ?? false, role: arg.role, runVar: arg.runVar ?? false, - consistantChar: arg.consistantChar ?? false + consistantChar: arg.consistantChar ?? false, } @@ -1540,14 +1844,25 @@ export function risuChatParser(da:string, arg:{ break } if(stackType[nested.length] === 6){ - console.log(dat) const sft = nested.shift() nested[0] += sft + `{{${dat}}}` break } } - const mc = isPureMode() ? null :matcher(dat, matcherObj) - nested[0] += mc ?? `{{${dat}}}` + const mc = isPureMode() ? null :basicMatcher(dat, matcherObj, tempVar) + if(!mc && mc !== ''){ + nested[0] += `{{${dat}}}` + } + else if(typeof(mc) === 'string'){ + nested[0] += mc + } + else{ + nested[0] += mc.text + tempVar = mc.var + if(tempVar['__force_return__']){ + return tempVar['__return__'] ?? 'null' + } + } break } case '>':{ @@ -1653,24 +1968,10 @@ export function risuChatParser(da:string, arg:{ export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key:string]:string}):Promise<{ text:string, var:{[key:string]:string} -}> { +}|void|null> { const arra = p1.split('::') switch(arra[0]){ - case 'localvar': - case 'getlocalvar':{ - return { - text: vars[arra[1]] ?? '', - var: vars - } - } - case 'setlocalvar':{ - vars[arra[1]] = arra[2] - return { - text: '', - var: vars - } - } case 'pushchat': case 'addchat': case 'add_chat': @@ -1683,6 +1984,13 @@ export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key: var: vars } } + case 'yield':{ + vars['__return__'] = (vars['__return__'] ?? '') + arra[1] + return { + text: '', + var: vars + } + } case 'setchat': case 'set_chat':{ const chat = get(CurrentChat) @@ -1692,6 +2000,45 @@ export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key: } CurrentChat.set(chat) } + case 'set_chat_role': + case 'setchatrole':{ + const chat = get(CurrentChat) + const message = chat.message?.at(Number(arra[1])) + if(message){ + message.role = arra[2] === 'user' ? 'user' : 'char' + } + CurrentChat.set(chat) + } + case 'cutchat': + case 'cut_chat':{ + const chat = get(CurrentChat) + chat.message = chat.message.slice(Number(arra[1]), Number(arra[2])) + CurrentChat.set(chat) + return { + text: '', + var: vars + } + } + case 'insertchat': + case 'insert_chat':{ + const chat = get(CurrentChat) + chat.message.splice(Number(arra[1]), 0, {role: arra[2] === 'user' ? 'user' : 'char', data: arra[3] ?? ''}) + CurrentChat.set(chat) + return { + text: '', + var: vars + } + } + case 'removechat': + case 'remove_chat':{ + const chat = get(CurrentChat) + chat.message.splice(Number(arra[1]), 1) + CurrentChat.set(chat) + return { + text: '', + var: vars + } + } case 'regex':{ const reg = new RegExp(arra[1], arra[2]) const match = reg.exec(arra[3]) @@ -1713,6 +2060,7 @@ export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key: } } case 'call':{ + //WIP const called = await risuCommandParser(arra[1], { db: matcherArg.db, chara: matcherArg.chara, @@ -1726,27 +2074,147 @@ export async function commandMatcher(p1:string,matcherArg:matcherArg,vars:{[key: var: vars } } - case 'yield':{ - vars['__return__'] = (vars['__return__'] ?? '') + arra[1] + case 'stop_chat':{ + vars['__stop_chat__'] = '1' return { text: '', var: vars } } - case 'return':{ - vars['__return__'] = arra[1] - vars['__force_return__'] = '1' + case 'similaritysearch': + case 'similarity_search': + case 'similarity':{ + if(!matcherArg.lowLevelAccess){ + return { + text: '', + var: vars + } + } + const processer = new HypaProcesser('MiniLM') + const source = arra[1] + const target = parseArray(arra[2]) + await processer.addText(target) + const result = await processer.similaritySearch(source) + return { + text: makeArray(result), + var: vars + } + } + case 'image_generation': + case 'imagegeneration': + case 'imagegen': + case 'image_gen':{ + if(!matcherArg.lowLevelAccess){ + return { + text: '', + var: vars + } + } + const prompt = arra[1] + const negative = arra[2] || '' + const char = matcherArg.chara + + const gen = await generateAIImage(prompt, char as character, negative, 'inlay') + if(!gen){ + return { + text: 'ERROR: Image generation failed', + var: vars + } + } + const imgHTML = new Image() + imgHTML.src = gen + const inlay = await writeInlayImage(imgHTML) + return { + text: inlay, + var: vars + } + } + case 'send_llm': + case 'llm':{ + if(!matcherArg.lowLevelAccess){ + return { + text: '', + var: vars + } + } + const prompt = parseArray(arra[1]) + let promptbody:OpenAIChat[] = prompt.map((f) => { + const dict = parseDict(f) + let role:'system'|'user'|'assistant' = 'assistant' + switch(dict['role']){ + case 'system': + case 'sys': + role = 'system' + break + case 'user': + role = 'user' + break + case 'assistant': + case 'bot': + case 'char':{ + role = 'assistant' + break + } + } + + return { + content: dict['content'] ?? '', + role: role, + } + }) + const result = await requestChatData({ + formated: promptbody, + bias: {}, + useStreaming: false, + noMultiGen: true, + }, 'model') + + if(result.type === 'fail'){ + return { + text: 'ERROR: ' + result.result, + var: vars + } + } + + if(result.type === 'streaming' || result.type === 'multiline'){ + return { + text: 'ERROR: Streaming and multiline is not supported in this context', + var: vars + } + } + + return { + text: result.result, + var: vars + } + } + case 'alert':{ + alertNormal(arra[1]) return { text: '', var: vars } } + case 'input': + case 'alert_input': + case 'alertinput':{ + const input = await alertInput(arra[1]) + return { + text: input, + var: vars + } + } } - return { - text: matcher(p1, matcherArg).toString(), - var: vars + const matched = basicMatcher(p1, matcherArg) + if(typeof(matched) === 'string'){ + return { + text: matched, + var: vars + } } + + return matched } @@ -1756,6 +2224,7 @@ export async function risuCommandParser(da:string, arg:{ funcName?:string passed?:string[], recursiveCount?:number + lowLevelAccess?:boolean } = {}):Promise<{[key:string]:string}>{ const db = arg.db ?? get(DataBase) const aChara = arg.chara @@ -1798,7 +2267,8 @@ export async function risuCommandParser(da:string, arg:{ consistantChar: false, funcName: arg.funcName ?? null, text: da, - recursiveCount: recursiveCount + recursiveCount: recursiveCount, + lowLevelAccess: arg.lowLevelAccess ?? false } let tempVar:{[key:string]:string} = {} @@ -1882,7 +2352,6 @@ export async function risuCommandParser(da:string, arg:{ break } if(stackType[nested.length] === 6){ - console.log(dat) const sft = nested.shift() nested[0] += sft + `{{${dat}}}` break @@ -1892,11 +2361,16 @@ export async function risuCommandParser(da:string, arg:{ text:null, var:tempVar } : (await commandMatcher(dat, matcherObj, tempVar)) - tempVar = mc.var - if(tempVar['__force_return__']){ - return tempVar + if(mc && (mc.text || mc.text === '')){ + tempVar = mc.var + if(tempVar['__force_return__']){ + return tempVar + } + nested[0] += mc.text ?? `{{${dat}}}` + } + else{ + nested[0] += `{{${dat}}}` } - nested[0] += mc.text ?? `{{${dat}}}` break } default:{ @@ -2012,4 +2486,84 @@ export async function promptTypeParser(prompt:string):Promise f !== '').map((v) => { + let role:'system'|'user'|'assistant' = 'user' + //default separators + if(v.startsWith('user' + seperator)){ + role = 'user' + v = v.substring(4 + seperator.length) + } + else if(v.startsWith('system' + seperator)){ + role = 'system' + v = v.substring(6 + seperator.length) + } + else if(v.startsWith('assistant' + seperator)){ + role = 'assistant' + v = v.substring(9 + seperator.length) + } + //space/newline separators + else if(v.startsWith('user ') || v.startsWith('user\n')){ + role = 'user' + v = v.substring(5) + } + else if(v.startsWith('system ') || v.startsWith('system\n')){ + role = 'system' + v = v.substring(7) + } + else if(v.startsWith('assistant ') || v.startsWith('assistant\n')){ + role = 'assistant' + v = v.substring(10) + } + + v = v.trim() + + if(v.endsWith(ender)){ + v = v.substring(0, v.length - ender.length) + } + + return { + role: role, + content: v + } + }) } \ No newline at end of file diff --git a/src/ts/persona.ts b/src/ts/persona.ts index 50e8b62d..eb349cc3 100644 --- a/src/ts/persona.ts +++ b/src/ts/persona.ts @@ -1,11 +1,12 @@ import { get } from "svelte/store" import { DataBase, saveImage, setDatabase } from "./storage/database" -import { selectSingleFile, sleep } from "./util" +import { getUserName, selectSingleFile, sleep } from "./util" import { alertError, alertNormal, alertStore } from "./alert" import { downloadFile, readImage } from "./storage/globalApi" import { language } from "src/lang" import { reencodeImage } from "./process/files/image" import { PngChunk } from "./pngChunk" +import { v4 } from "uuid" export async function selectUserImg() { const selected = await selectSingleFile(['png']) @@ -19,19 +20,18 @@ export async function selectUserImg() { db.personas[db.selectedPersona] = { name: db.username, icon: db.userIcon, - personaPrompt: db.personaPrompt + personaPrompt: db.personaPrompt, + id: v4() } setDatabase(db) } export function saveUserPersona() { let db = get(DataBase) - db.personas[db.selectedPersona] = { - name: db.username, - icon: db.userIcon, - personaPrompt: db.personaPrompt, - largePortrait: db.personas[db.selectedPersona]?.largePortrait, - } + db.personas[db.selectedPersona].name=db.username + db.personas[db.selectedPersona].icon=db.userIcon, + db.personas[db.selectedPersona].personaPrompt=db.personaPrompt, + db.personas[db.selectedPersona].largePortrait=db.personas[db.selectedPersona]?.largePortrait, setDatabase(db) } @@ -111,7 +111,8 @@ export async function importUserPersona(){ db.personas.push({ name: data.name, icon: await saveImage(await reencodeImage(v.data)), - personaPrompt: data.personaPrompt + personaPrompt: data.personaPrompt, + id: v4() }) setDatabase(db) alertNormal(language.successImport) diff --git a/src/ts/plugins/automark.ts b/src/ts/plugins/automark.ts deleted file mode 100644 index cfee0bc4..00000000 --- a/src/ts/plugins/automark.ts +++ /dev/null @@ -1,206 +0,0 @@ - -const excludesDat = ['<','>','{','}','[',']','(',')','-',':',';','…','—','–','_','*','+','/','\\','|','!','?','.',',',' '] -const symbols = ['<','>','{','}','[',']','(',')','-',':',';','…','—','–','_','*','+','/','\\','|','!','?','.',',',' ', '\n', '。', '、', '!', '?', ',', ';', ':', '(', ')', '【', '】', '「', '」', '『', '』', '“', '”', '‘', '’', '《', '》', '〈', '〉', '‹', '›', '«', '»', '‟', '„'] - -const selfClosingTags = [ - 'br','hr','img','input','meta','link','base','area','col','command','embed','keygen','param','source','track','wbr', - //self closing tags defined by HTML5 - '!', - //for doctype and comment - 'user', 'bot', 'char' - //special tags for user, bot, and char -] - -const checkSelfClosingTag = (dat:string) => { - dat = dat.substring(0, 10) //we only need to check the first 10 characters, to avoid checking the whole string - dat = dat.toLowerCase() //we don't care about the case - for(const tag of selfClosingTags){ - if(dat.startsWith(tag)){ - return true - } - } - return false -} - -export function risuFormater(dat:string){ - const lines:[string,string][] = [['','']] // [type, content] - let htmlType = 0 // 0: not inside tag, 1: closing tag, 2: opening tag - for(let i=0;i { - return lines[lines.length-1] ?? [ - 'not-found', '' - ] - } - //html tag handling - if(dat[i] === '<' && getLastLine()[0] !== 'code-block'){ - lines.push(['html-tag','']) - if(dat[i+1] === '/'){ - htmlType = 1 - } - else{ - htmlType = 2 - } - } - - if(dat[i] === '>' && getLastLine()[0] === 'html-tag'){ - const pop = lines.pop() - const tagAttr = pop[1].substring(1).trim() - if(htmlType === 1){ - const pop2 = lines.pop() //probably html-inner - const chunk = pop2[1] + pop[1] + '>' - if(getLastLine()[0] === ''){ - lines.push(['html-chunk',chunk]) - lines.push(['','']) - } - else{ - getLastLine()[1] += chunk - } - continue - } - else if(checkSelfClosingTag(tagAttr)){ - const chunk = pop[1] + '>' - if(getLastLine()[0] === ''){ - lines.push(['html-chunk',chunk]) - lines.push(['','']) - } - else{ - getLastLine()[1] += chunk - } - continue - } - else{ - lines.push(['html-inner',pop[1]]) - } - htmlType = 0 - } - - //code block handling - - if(dat[i] === '`' && dat[i+1] === '`' && dat[i+2] === '`' && getLastLine()[0] === ''){ - if(getLastLine()[0] === 'code-block'){ - getLastLine()[1] += '```' - lines.push(['','']) - } - else{ - lines.push(['code-block','```']) - } - i += 2 - continue - } - - - if(dat[i] === '\n' && getLastLine()[0] === ''){ - lines.push(['newline','\n']) - lines.push(['','']) - } - else if(lines[lines.length-1]){ - lines[lines.length-1][1] += dat[i] - } - } - - let result = '' - for(let i=0;i') || line.endsWith('}') || line.startsWith('<')){ - endMarked = true - } - - if(isNumbered || endMarked){ - result += line - continue - } - - let depth = 0 - let depthChunk:string[] = [''] - let depthChunkType:string[] = [''] - - //spaces for detection - line = ' ' + line + ' ' - - const isNotCharacter = (t:string) => { - return symbols.includes(t) - } - - for(let j=0;j${pop}${line[j]}` - } - else{ - depthChunkType.push('"') - depthChunk.push(line[j]) - depth++ - } - break - } - case "'": - case '‘': - case '’':{ - if(depthChunkType[depth] === "'"){ - if(isNotCharacter(line[j-1]) || !isNotCharacter(line[j+1]) || (line[j-2] === 'i' && line[j-1] === 'n')){ - //this is not a quote - depthChunk[depth] += line[j] - } - else{ - depthChunkType.pop() - const pop = depthChunk.pop() - depth-- - depthChunk[depth] += `${pop}${line[j]}` - } - } - else{ - if(!isNotCharacter(line[j-1]) || isNotCharacter(line[j+1])){ - //this is not a quote - depthChunk[depth] += line[j] - } - else{ - depthChunkType.push("'") - depthChunk.push(line[j]) - depth++ - } - } - break - - } - - default:{ - depthChunk[depth] += line[j] - } - } - } - - let lineResult = '' - - while(depthChunk.length > 0){ - lineResult = depthChunk.pop() + lineResult - } - - if(lineResult.startsWith(' ')){ - lineResult = lineResult.substring(1) - } - if(lineResult.endsWith(' ')){ - lineResult = lineResult.substring(0,lineResult.length-1) - } - - result += lineResult - } - - return result.trim() -} \ No newline at end of file diff --git a/src/ts/process/embedding/addinfo.ts b/src/ts/process/embedding/addinfo.ts index 1383dc77..4913ceb3 100644 --- a/src/ts/process/embedding/addinfo.ts +++ b/src/ts/process/embedding/addinfo.ts @@ -3,6 +3,7 @@ import { HypaProcesser } from '../memory/hypamemory' import type { OpenAIChat } from ".."; import { stringlizeChat } from "../stringlize"; import { get } from "svelte/store"; +import { getUserName } from "src/ts/util"; export async function additionalInformations(char: character,chats:Chat,){ const processer = new HypaProcesser('MiniLM') @@ -18,7 +19,7 @@ export async function additionalInformations(char: character,chats:Chat,){ if(!name){ if(chat.role === 'user'){ - name = db.username + name = getUserName() } else{ name = char.name diff --git a/src/ts/process/index.ts b/src/ts/process/index.ts index 4da73d91..483da9a8 100644 --- a/src/ts/process/index.ts +++ b/src/ts/process/index.ts @@ -5,7 +5,7 @@ import { ChatTokenizer, tokenize, tokenizeNum } from "../tokenizer"; import { language } from "../../lang"; import { alertError } from "../alert"; import { loadLoreBookPrompt, loadLoreBookV3Prompt } from "./lorebook"; -import { findCharacterbyId, getAuthorNoteDefaultText, isLastCharPunctuation, trimUntilPunctuation } from "../util"; +import { findCharacterbyId, getAuthorNoteDefaultText, getPersonaPrompt, getUserName, isLastCharPunctuation, trimUntilPunctuation } from "../util"; import { requestChatData } from "./request"; import { stableDiff } from "./stableDiff"; import { processScript, processScriptFull, risuChatParser } from "./scripts"; @@ -27,6 +27,7 @@ import { addRerolls } from "./prereroll"; import { runImageEmbedding } from "./transformers"; import { hanuraiMemory } from "./memory/hanuraiMemory"; import { hypaMemoryV2 } from "./memory/hypav2"; +import { runLuaEditTrigger } from "./lua"; export interface OpenAIChat{ role: 'system'|'user'|'assistant'|'function' @@ -371,7 +372,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n if(db.personaPrompt){ unformated.personaPrompt.push({ role: 'system', - content: risuChatParser(db.personaPrompt, {chara: currentChar}) + content: risuChatParser(getPersonaPrompt(), {chara: currentChar}) }) } @@ -573,7 +574,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n } } - const examples = exampleMessage(currentChar, db.username) + const examples = exampleMessage(currentChar, getUserName()) for(const example of examples){ currentTokens += await tokenizer.tokenizeChat(example) @@ -633,7 +634,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n } } else if(msg.role === 'user'){ - name = `${db.username}` + name = `${getUserName()}` } if(!msg.chatId){ msg.chatId = v4() @@ -1046,6 +1047,8 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n data: formated }) + formated = await runLuaEditTrigger(currentChar, 'editRequest', formated) + //token rechecking let inputTokens = 0 diff --git a/src/ts/process/infunctions.ts b/src/ts/process/infunctions.ts index 40dd0076..39b7e6c0 100644 --- a/src/ts/process/infunctions.ts +++ b/src/ts/process/infunctions.ts @@ -1,4 +1,4 @@ -import { getChatVar } from "../parser"; +import { getChatVar, getGlobalChatVar } from "../parser"; function toRPN(expression:string) { let outputQueue = ''; @@ -114,6 +114,13 @@ function executeRPNCalculation(text:string) { return "0" } return parsed.toString() + }).replace(/\@([a-zA-Z0-9_]+)/g, (_, p1) => { + const v = getGlobalChatVar(p1) + const parsed = parseFloat(v) + if(isNaN(parsed)){ + return "0" + } + return parsed.toString() }).replace(/&&/g, '&').replace(/\|\|/g, '|').replace(/<=/g, '≤').replace(/>=/g, '≥').replace(/==/g, '=').replace(/null/gi, '0') const expression = toRPN(text); const evaluated = calculateRPN(expression); diff --git a/src/ts/process/lua.ts b/src/ts/process/lua.ts new file mode 100644 index 00000000..60b7694d --- /dev/null +++ b/src/ts/process/lua.ts @@ -0,0 +1,664 @@ +import { getChatVar, risuChatParser, setChatVar, type simpleCharacterArgument } from "../parser"; +import { LuaEngine, LuaFactory } from "wasmoon"; +import { DataBase, setDatabase, type Chat, type character, type groupChat } from "../storage/database"; +import { get } from "svelte/store"; +import { CurrentCharacter, CurrentChat, CurrentVariablePointer, ReloadGUIPointer, selectedCharID } from "../stores"; +import { alertError, alertInput, alertNormal } from "../alert"; +import { HypaProcesser } from "./memory/hypamemory"; +import { generateAIImage } from "./stableDiff"; +import { writeInlayImage } from "./files/image"; +import type { OpenAIChat } from "."; +import { requestChatData } from "./request"; +import { v4 } from "uuid"; +import { getModuleTriggers } from "./modules"; +import { Mutex } from "../mutex"; + +let luaFactory:LuaFactory +let LuaSafeIds = new Set() +let LuaEditDisplayIds = new Set() +let LuaLowLevelIds = new Set() + +interface LuaEngineState { + code: string; + engine: LuaEngine; + mutex: Mutex; +} + +let LuaEngines = new Map() + +export async function runLua(code:string, arg:{ + char?:character|groupChat|simpleCharacterArgument, + chat?:Chat + setVar?: (key:string, value:string) => void, + getVar?: (key:string) => string, + lowLevelAccess?: boolean, + mode?: string, + data?: any +}){ + const char = arg.char ?? get(CurrentCharacter) + const setVar = arg.setVar ?? setChatVar + const getVar = arg.getVar ?? getChatVar + const mode = arg.mode ?? 'manual' + const data = arg.data ?? {} + let chat = arg.chat ?? get(CurrentChat) + let stopSending = false + let lowLevelAccess = arg.lowLevelAccess ?? false + + if(!luaFactory){ + await makeLuaFactory() + } + let luaEngineState = LuaEngines.get(mode) + let wasEmpty = false + if (!luaEngineState) { + luaEngineState = { + code, + engine: await luaFactory.createEngine({injectObjects: true}), + mutex: new Mutex() + } + LuaEngines.set(mode, luaEngineState) + wasEmpty = true + } + return await luaEngineState.mutex.runExclusive(async () => { + if (wasEmpty || code !== luaEngineState.code) { + if (!wasEmpty) + luaEngineState.engine.global.close() + luaEngineState.engine = await luaFactory.createEngine({injectObjects: true}) + const luaEngine = luaEngineState.engine + luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => { + if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){ + return + } + setVar(key, value) + }) + luaEngine.global.set('getChatVar', (id:string,key:string) => { + if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){ + return + } + return getVar(key) + }) + luaEngine.global.set('stopChat', (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + stopSending = true + }) + luaEngine.global.set('alertError', (id:string, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + alertError(value) + }) + luaEngine.global.set('alertNormal', (id:string, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + alertNormal(value) + }) + luaEngine.global.set('alertInput', (id:string, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + return alertInput(value) + }) + luaEngine.global.set('setChat', (id:string, index:number, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const message = chat.message?.at(index) + if(message){ + message.data = value + } + CurrentChat.set(chat) + }) + luaEngine.global.set('setChatRole', (id:string, index:number, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const message = chat.message?.at(index) + if(message){ + message.role = value === 'user' ? 'user' : 'char' + } + CurrentChat.set(chat) + }) + luaEngine.global.set('cutChat', (id:string, start:number, end:number) => { + if(!LuaSafeIds.has(id)){ + return + } + chat.message = chat.message.slice(start,end) + CurrentChat.set(chat) + }) + luaEngine.global.set('removeChat', (id:string, index:number) => { + if(!LuaSafeIds.has(id)){ + return + } + chat.message.splice(index, 1) + CurrentChat.set(chat) + }) + luaEngine.global.set('addChat', (id:string, role:string, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + let roleData:'user'|'char' = role === 'user' ? 'user' : 'char' + chat.message.push({role: roleData, data: value}) + CurrentChat.set(chat) + }) + luaEngine.global.set('insertChat', (id:string, index:number, role:string, value:string) => { + if(!LuaSafeIds.has(id)){ + return + } + let roleData:'user'|'char' = role === 'user' ? 'user' : 'char' + chat.message.splice(index, 0, {role: roleData, data: value}) + CurrentChat.set(chat) + }) + luaEngine.global.set('removeChat', (id:string, index:number) => { + if(!LuaSafeIds.has(id)){ + return + } + chat.message.splice(index, 1) + CurrentChat.set(chat) + }) + luaEngine.global.set('getChatLength', (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + return chat.message.length + }) + luaEngine.global.set('getFullChatMain', (id:string) => { + const data = JSON.stringify(chat.message.map((v) => { + return { + role: v.role, + data: v.data + } + })) + return data + }) + + luaEngine.global.set('setFullChatMain', (id:string, value:string) => { + const realValue = JSON.parse(value) + if(!LuaSafeIds.has(id)){ + return + } + chat.message = realValue.map((v) => { + return { + role: v.role, + data: v.data + } + }) + CurrentChat.set(chat) + }) + + luaEngine.global.set('logMain', (value:string) => { + console.log(JSON.parse(value)) + }) + + luaEngine.global.set('reloadDisplay', (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + ReloadGUIPointer.set(get(ReloadGUIPointer) + 1) + }) + + //Low Level Access + luaEngine.global.set('similarity', async (id:string, source:string, value:string[]) => { + if(!LuaLowLevelIds.has(id)){ + return + } + const processer = new HypaProcesser('MiniLM') + await processer.addText(value) + return await processer.similaritySearch(source) + }) + + luaEngine.global.set('generateImage', async (id:string, value:string, negValue:string = '') => { + if(!LuaLowLevelIds.has(id)){ + return + } + const gen = await generateAIImage(value, char as character, negValue, 'inlay') + if(!gen){ + return 'Error: Image generation failed' + } + const imgHTML = new Image() + imgHTML.src = gen + const inlay = await writeInlayImage(imgHTML) + return `{{inlay::${inlay}}}` + }) + + luaEngine.global.set('LLMMain', async (id:string, promptStr:string) => { + let prompt:{ + role: string, + content: string + }[] = JSON.parse(promptStr) + if(!LuaLowLevelIds.has(id)){ + return + } + let promptbody:OpenAIChat[] = prompt.map((dict) => { + let role:'system'|'user'|'assistant' = 'assistant' + switch(dict['role']){ + case 'system': + case 'sys': + role = 'system' + break + case 'user': + role = 'user' + break + case 'assistant': + case 'bot': + case 'char':{ + role = 'assistant' + break + } + } + + return { + content: dict['content'] ?? '', + role: role, + } + }) + const result = await requestChatData({ + formated: promptbody, + bias: {}, + useStreaming: false, + noMultiGen: true, + }, 'model') + + if(result.type === 'fail'){ + return JSON.stringify({ + success: false, + result: 'Error: ' + result.result + }) + } + + if(result.type === 'streaming' || result.type === 'multiline'){ + return JSON.stringify({ + success: false, + result: result.result + }) + } + + return JSON.stringify({ + success: true, + result: result.result + }) + }) + + luaEngine.global.set('simpleLLM', async (id:string, prompt:string) => { + if(!LuaLowLevelIds.has(id)){ + return + } + const result = await requestChatData({ + formated: [{ + role: 'user', + content: prompt + }], + bias: {}, + useStreaming: false, + noMultiGen: true, + }, 'model') + + if(result.type === 'fail'){ + return { + success: false, + result: 'Error: ' + result.result + } + } + + if(result.type === 'streaming' || result.type === 'multiline'){ + return { + success: false, + result: result.result + } + } + + return { + success: true, + result: result.result + } + }) + + luaEngine.global.set('getName', async (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + return char.name + }) + + luaEngine.global.set('setName', async (id:string, name:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + if(typeof name !== 'string'){ + throw('Invalid data type') + } + db.characters[selectedChar].name = name + setDatabase(db) + }) + + luaEngine.global.set('setDescription', async (id:string, desc:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char =db.characters[selectedChar] + if(typeof data !== 'string'){ + throw('Invalid data type') + } + if(char.type === 'group'){ + throw('Character is a group') + } + char.desc = desc + db.characters[selectedChar] = char + setDatabase(db) + }) + + luaEngine.global.set('setCharacterFirstMessage', async (id:string, data:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + if(typeof data !== 'string'){ + return false + } + char.firstMessage = data + db.characters[selectedChar] = char + setDatabase(db) + return true + }) + + luaEngine.global.set('getCharacterFirstMessage', async (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + return char.firstMessage + }) + + luaEngine.global.set('getBackgroundEmbedding', async (id:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + return char.backgroundHTML + }) + + luaEngine.global.set('setBackgroundEmbedding', async (id:string, data:string) => { + if(!LuaSafeIds.has(id)){ + return + } + const db = get(DataBase) + const selectedChar = get(selectedCharID) + if(typeof data !== 'string'){ + return false + } + db.characters[selectedChar].backgroundHTML = data + setDatabase(db) + return true + }) + + await luaEngine.doString(luaCodeWarper(code)) + luaEngineState.code = code + } + let accessKey = v4() + if(mode === 'editDisplay'){ + LuaEditDisplayIds.add(accessKey) + } + else{ + LuaSafeIds.add(accessKey) + if(lowLevelAccess){ + LuaLowLevelIds.add(accessKey) + } + } + let res:any + const luaEngine = luaEngineState.engine + try { + switch(mode){ + case 'input':{ + const func = luaEngine.global.get('onInput') + if(func){ + res = await func(accessKey) + } + break + } + case 'output':{ + const func = luaEngine.global.get('onOutput') + if(func){ + res = await func(accessKey) + } + break + } + case 'start':{ + const func = luaEngine.global.get('onStart') + if(func){ + res = await func(accessKey) + } + break + } + case 'editRequest': + case 'editDisplay': + case 'editInput': + case 'editOutput':{ + const func = luaEngine.global.get('callListenMain') + if(func){ + res = await func(mode, accessKey, JSON.stringify(data)) + res = JSON.parse(res) + } + break + } + default:{ + const func = luaEngine.global.get(mode) + if(func){ + res = await func(accessKey) + } + break + } + } + if(res === false){ + stopSending = true + } + } catch (error) { + console.error(error) + } + + LuaSafeIds.delete(accessKey) + LuaLowLevelIds.delete(accessKey) + + return { + stopSending, chat, res + } + }) +} + +async function makeLuaFactory(){ + luaFactory = new LuaFactory() + async function mountFile(name:string){ + let code = '' + for(let i = 0; i < 3; i++){ + try { + const res = await fetch('/lua/' + name) + if(res.status >= 200 && res.status < 300){ + code = await res.text() + break + } + } catch (error) {} + } + await luaFactory.mountFile(name,code) + } + + await mountFile('json.lua') +} + +function luaCodeWarper(code:string){ + return ` +json = require 'json' + +function getFullChat(id) + return json.decode(getFullChatMain(id)) +end + +function setFullChat(id, value) + setFullChatMain(id, json.encode(value)) +end + +function log(value) + logMain(json.encode(value)) +end + +function LLM(id, prompt) + return json.decode(LLMMain(id, json.encode(prompt)):await()) +end + +local editRequestFuncs = {} +local editDisplayFuncs = {} +local editInputFuncs = {} +local editOutputFuncs = {} + +function listenEdit(type, func) + if type == 'editRequest' then + editRequestFuncs[#editRequestFuncs + 1] = func + return + end + + if type == 'editDisplay' then + editDisplayFuncs[#editDisplayFuncs + 1] = func + return + end + + if type == 'editInput' then + editInputFuncs[#editInputFuncs + 1] = func + return + end + + if type == 'editOutput' then + editOutputFuncs[#editOutputFuncs + 1] = func + return + end + + throw('Invalid type') +end + +function getState(id, name) + local escapedName = "__"..name + return json.decode(getChatVar(id, escapedName)) +end + +function setState(id, name, value) + local escapedName = "__"..name + setChatVar(id, escapedName, json.encode(value)) +end + +function async(callback) + return function(...) + local co = coroutine.create(callback) + local safe, result = coroutine.resume(co, ...) + + return Promise.create(function(resolve, reject) + local checkresult + local step = function() + if coroutine.status(co) == "dead" then + local send = safe and resolve or reject + return send(result) + end + + safe, result = coroutine.resume(co) + checkresult() + end + + checkresult = function() + if safe and result == Promise.resolve(result) then + result:finally(step) + else + step() + end + end + + checkresult() + end) + end +end + +callListenMain = async(function(type, id, value) + local realValue = json.decode(value) + + if type == 'editRequest' then + for _, func in ipairs(editRequestFuncs) do + realValue = func(id, realValue) + end + end + + if type == 'editDisplay' then + for _, func in ipairs(editDisplayFuncs) do + realValue = func(id, realValue) + print(realValue) + end + end + + if type == 'editInput' then + for _, func in ipairs(editInputFuncs) do + realValue = func(id, realValue) + end + end + + if type == 'editOutput' then + for _, func in ipairs(editOutputFuncs) do + realValue = func(id, realValue) + end + end + + return json.encode(realValue) +end) + +${code} +` +} + +export async function runLuaEditTrigger(char:character|groupChat|simpleCharacterArgument, mode:string, content:T):Promise{ + let data = content + + switch(mode){ + case 'editinput': + mode = 'editInput' + break + case 'editoutput': + mode = 'editOutput' + break + case 'editdisplay': + mode = 'editDisplay' + break + case 'editprocess': + return content + } + + try { + const triggers = char.type === 'group' ? (getModuleTriggers()) : (char.triggerscript.map((v) => { + v.lowLevelAccess = false + return v + }).concat(getModuleTriggers())) + + for(let trigger of triggers){ + if(trigger?.effect?.[0]?.type === 'triggerlua'){ + const runResult = await runLua(trigger.effect[0].code, { + char: char, + lowLevelAccess: false, + mode: mode, + data: data + }) + data = runResult.res ?? data + } + } + + + return data + } catch (error) { + return content + } +} \ No newline at end of file diff --git a/src/ts/process/memory/supaMemory.ts b/src/ts/process/memory/supaMemory.ts index 19f1f209..a890a220 100644 --- a/src/ts/process/memory/supaMemory.ts +++ b/src/ts/process/memory/supaMemory.ts @@ -7,6 +7,7 @@ import { HypaProcesser } from "./hypamemory"; import { stringlizeChat } from "../stringlize"; import { globalFetch } from "src/ts/storage/globalApi"; import { runSummarizer } from "../transformers"; +import { getUserName } from "src/ts/util"; export async function supaMemory( chats:OpenAIChat[], @@ -329,7 +330,7 @@ export async function supaMemory( if((chunkSize + tokens) > maxChunkSize){ if(stringlizedChat === ''){ if(cont.role !== 'function' && cont.role !== 'system'){ - stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : db.username}: ${cont.content}\n\n` + stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : getUserName()}: ${cont.content}\n\n` spiceLen += 1 currentTokens -= tokens chunkSize += tokens @@ -338,7 +339,7 @@ export async function supaMemory( lastId = cont.memo break } - stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : db.username}: ${cont.content}\n\n` + stringlizedChat += `${cont.role === 'assistant' ? char.type === 'group' ? '' : char.name : getUserName()}: ${cont.content}\n\n` spiceLen += 1 currentTokens -= tokens chunkSize += tokens diff --git a/src/ts/process/models/nai.ts b/src/ts/process/models/nai.ts index b366d1ce..7eaa117d 100644 --- a/src/ts/process/models/nai.ts +++ b/src/ts/process/models/nai.ts @@ -3,7 +3,7 @@ import type { OpenAIChat } from ".." import { get } from "svelte/store" import { globalFetch } from "src/ts/storage/globalApi" import { alertError, alertInput, alertNormal, alertWait } from "src/ts/alert" -import { sleep } from "src/ts/util" +import { getUserName, sleep } from "src/ts/util" export function stringlizeNAIChat(formated:OpenAIChat[], char:string, continued: boolean){ const db = get(DataBase) @@ -34,7 +34,7 @@ export function stringlizeNAIChat(formated:OpenAIChat[], char:string, continued: res += '> ' } if(db.NAIappendName){ - res += db.username + ": " + res += getUserName() + ": " } res += form.content resultString.push(res) diff --git a/src/ts/process/modules.ts b/src/ts/process/modules.ts index 6aca9330..7965fb37 100644 --- a/src/ts/process/modules.ts +++ b/src/ts/process/modules.ts @@ -17,6 +17,8 @@ export interface RisuModule{ trigger?: triggerscript[] id: string lowLevelAccess?: boolean + hideIcon?: boolean + backgroundEmbedding?:string } export async function exportModule(module:RisuModule){ diff --git a/src/ts/process/request.ts b/src/ts/process/request.ts index dd0d0a30..298a1e6f 100644 --- a/src/ts/process/request.ts +++ b/src/ts/process/request.ts @@ -1525,11 +1525,23 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model' let lastChatPrompt = '' let preamble = '' - const lastChat = formated[formated.length-1] + let lastChat = formated[formated.length-1] if(lastChat.role === 'user'){ lastChatPrompt = lastChat.content formated.pop() } + else{ + while(lastChat.role !== 'user'){ + lastChat = formated.pop() + if(!lastChat){ + return { + type: 'fail', + result: 'Cohere requires a user message to generate a response' + } + } + lastChatPrompt = (lastChat.role === 'user' ? '' : `${lastChat.role}: `) + '\n' + lastChat.content + lastChatPrompt + } + } const firstChat = formated[0] if(firstChat.role === 'system'){ @@ -1537,29 +1549,36 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model' formated.shift() } + //reformat chat + + + + let body = { message: lastChatPrompt, chat_history: formated.map((v) => { if(v.role === 'assistant'){ return { role: 'CHATBOT', - content: v.content + message: v.content } } if(v.role === 'system'){ return { role: 'SYSTEM', - content: v.content + message: v.content } } if(v.role === 'user'){ return { role: 'USER', - content: v.content + message: v.content } } return null - }).filter((v) => v !== null), + }).filter((v) => v !== null).filter((v) => { + return v.message + }), temperature: temperature, k: db.top_k, p: (db.top_p > 0.99) ? 0.99 : (db.top_p < 0.01) ? 0.01 : db.top_p, @@ -1568,10 +1587,17 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model' } if(preamble){ - // @ts-ignore - body.preamble = preamble + if(body.chat_history.length > 0){ + // @ts-ignore + body.preamble = preamble + } + else{ + body.message = `system: ${preamble}` + } } + console.log(body) + const res = await globalFetch('https://api.cohere.com/v1/chat', { method: "POST", headers: { @@ -1818,10 +1844,15 @@ export async function requestChatDataMain(arg:requestDataArgument, model:'model' "anthropic.claude-v2:1", "anthropic.claude-3-haiku-20240307-v1:0", "anthropic.claude-3-sonnet-20240229-v1:0", + "anthropic.claude-3-5-sonnet-20240620-v1:0", "anthropic.claude-3-opus-20240229-v1:0" ]; - const awsModel = raiModel.includes("opus") ? modelIDs[4] : raiModel.includes("sonnet") ? modelIDs[3] : modelIDs[2]; + const awsModel = + raiModel.includes("3-opus") ? modelIDs[5] : + raiModel.includes("3-5-sonnet") ? modelIDs[4] : + raiModel.includes("3-sonnet") ? modelIDs[3] : + modelIDs[2]; const url = `https://${host}/model/${awsModel}/invoke${stream ? "-with-response-stream" : ""}` const params = { diff --git a/src/ts/process/scripts.ts b/src/ts/process/scripts.ts index b502e3f1..488329a9 100644 --- a/src/ts/process/scripts.ts +++ b/src/ts/process/scripts.ts @@ -9,6 +9,7 @@ import { assetRegex, risuChatParser as risuChatParserOrg, type simpleCharacterAr import { runCharacterJS } from "../plugins/embedscript"; import { getModuleRegexScripts } from "./modules"; import { HypaProcesser } from "./memory/hypamemory"; +import { runLuaEditTrigger } from "./lua"; const dreg = /{{data}}/g const randomness = /\|\|\|/g @@ -56,6 +57,8 @@ export async function importRegex(){ } } +let bestMatchCache = new Map() + export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1){ let db = get(DataBase) let emoChanged = false @@ -65,6 +68,7 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter mode, data, }) + data = await runLuaEditTrigger(char, mode, data) if(scripts.length === 0){ return {data, emoChanged} } @@ -193,7 +197,7 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter } } else{ - data = risuChatParser(data.replace(reg, outScript)) + data = risuChatParser(data.replace(reg, outScript), { chatID: chatID }) } } } @@ -210,11 +214,18 @@ export async function processScriptFull(char:character|groupChat|simpleCharacter for(const match of matches){ const type = match[1] const assetName = match[2] - if(!assetNames.includes(assetName)){ - const searched = await processer.similaritySearch(assetName) - const bestMatch = searched[0] - if(bestMatch){ - data = data.replaceAll(match[0], `{{${type}::${bestMatch}}}`) + const cacheKey = char.chaId + '::' + assetName + if(type !== 'emotion' && type !== 'source'){ + if(bestMatchCache.has(cacheKey)){ + data = data.replaceAll(match[0], `{{${type}::${bestMatchCache.get(cacheKey)}}}`) + } + else if(!assetNames.includes(assetName)){ + const searched = await processer.similaritySearch(assetName) + const bestMatch = searched[0] + if(bestMatch){ + data = data.replaceAll(match[0], `{{${type}::${bestMatch}}}`) + bestMatchCache.set(cacheKey, bestMatch) + } } } } diff --git a/src/ts/process/stableDiff.ts b/src/ts/process/stableDiff.ts index aadd9705..fea4138c 100644 --- a/src/ts/process/stableDiff.ts +++ b/src/ts/process/stableDiff.ts @@ -413,6 +413,80 @@ export async function generateAIImage(genPrompt:string, currentChar:character, n return returnSdData + } + if(db.sdProvider === 'comfy'){ + const {workflow, posNodeID, posInputName, negNodeID, negInputName} = db.comfyConfig + const baseUrl = new URL(db.comfyUiUrl) + + const createUrl = (pathname: string, params: Record = {}) => { + const url = new URL(pathname, baseUrl) + url.search = new URLSearchParams(params).toString() + return url.toString() + } + + const fetchWrapper = async (url: string, options = {}) => { + console.log(url) + const response = await globalFetch(url, options) + if (!response.ok) { + console.log(JSON.stringify(response.data)) + throw new Error(JSON.stringify(response.data)) + } + return response.data + } + + try { + const prompt = JSON.parse(workflow) + prompt[posNodeID].inputs[posInputName] = genPrompt + prompt[negNodeID].inputs[negInputName] = neg + + const { prompt_id: id } = await fetchWrapper(createUrl('/prompt'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: { 'prompt': prompt } + }) + console.log(`prompt id: ${id}`) + + let item + + const startTime = Date.now() + const timeout = db.comfyConfig.timeout * 1000 + while (!(item = (await (await fetch(createUrl('/history'), { + headers: { 'Content-Type': 'application/json' }, + method: 'GET'})).json())[id])) { + console.log("Checking /history...") + if (Date.now() - startTime >= timeout) { + alertError("Error: Image generation took longer than expected."); + return false + } + await new Promise(r => setTimeout(r, 1000)) + } // Check history until the generation is complete. + const genImgInfo = Object.values(item.outputs).flatMap((output: any) => output.images)[0]; + + const imgResponse = await fetch(createUrl('/view', { + filename: genImgInfo.filename, + subfolder: genImgInfo.subfolder, + type: genImgInfo.type + }), { + headers: { 'Content-Type': 'application/json' }, + method: 'GET'}) + const img64 = Buffer.from(await imgResponse.arrayBuffer()).toString('base64') + + if(returnSdData === 'inlay'){ + return `data:image/png;base64,${img64}` + } + else { + let charemotions = get(CharEmotion) + const img = `data:image/png;base64,${img64}` + const emos:[string, string,number][] = [[img, img, Date.now()]] + charemotions[currentChar.chaId] = emos + CharEmotion.set(charemotions) + } + + return returnSdData + } catch (error) { + alertError(error) + return false + } } return '' -} \ No newline at end of file +} diff --git a/src/ts/process/stringlize.ts b/src/ts/process/stringlize.ts index ff58028f..ff5d7ac6 100644 --- a/src/ts/process/stringlize.ts +++ b/src/ts/process/stringlize.ts @@ -1,6 +1,7 @@ import { get } from "svelte/store"; import type { OpenAIChat } from "."; import { DataBase } from "../storage/database"; +import { getUserName } from "../util"; export function multiChatReplacer(){ @@ -57,7 +58,7 @@ export function stringlizeChatOba(formated:OpenAIChat[], characterName:string, s let name = form.name if(form.role === 'user'){ prefix = appendWhitespace(suggesting ? assistantPrefix : userPrefix, seperator) - name ??= `${db.username}` + name ??= `${getUserName()}` name += ': ' } else if(form.role === 'assistant'){ @@ -80,7 +81,7 @@ export function stringlizeChatOba(formated:OpenAIChat[], characterName:string, s if(!continued){ if(db.ooba.formating.useName){ if (suggesting){ - resultString.push(appendWhitespace(assistantPrefix, seperator) + `${db.username}:\n` + db.autoSuggestPrefix) + resultString.push(appendWhitespace(assistantPrefix, seperator) + `${getUserName()}:\n` + db.autoSuggestPrefix) } else { resultString.push(assistantPrefix + `${characterName}:`) } @@ -173,17 +174,17 @@ export function getUnstringlizerChunks(formated:OpenAIChat[], char:string, mode: chunks.push(`${char}: `) } } - if(db.username){ - charNames.push(db.username) + if(getUserName()){ + charNames.push(getUserName()) if(mode === 'ain'){ - chunks.push(`${db.username} `) - chunks.push(`${db.username} `) + chunks.push(`${getUserName()} `) + chunks.push(`${getUserName()} `) } else{ - chunks.push(`${db.username}:`) - chunks.push(`${db.username}:`) - chunks.push(`${db.username}: `) - chunks.push(`${db.username}: `) + chunks.push(`${getUserName()}:`) + chunks.push(`${getUserName()}:`) + chunks.push(`${getUserName()}: `) + chunks.push(`${getUserName()}: `) } } @@ -223,7 +224,7 @@ export function stringlizeAINChat(formated:OpenAIChat[], char:string, continued: resultString.push(form.content) } else if(form.role === 'user'){ - resultString.push(...formatToAIN(db.username, form.content)) + resultString.push(...formatToAIN(getUserName(), form.content)) } else if(form.name || form.role === 'assistant'){ resultString.push(...formatToAIN(form.name ?? char, form.content)) @@ -315,7 +316,7 @@ export function unstringlizeAIN(data:string,formated:OpenAIChat[], char:string = } } else{ - const role = (cont.character.trim() === db.username ? 'user' : 'char') + const role = (cont.character.trim() === getUserName() ? 'user' : 'char') result.push([ role, `「${cont.content}」` diff --git a/src/ts/process/templates/chatTemplate.ts b/src/ts/process/templates/chatTemplate.ts index 526e412b..71b969b2 100644 --- a/src/ts/process/templates/chatTemplate.ts +++ b/src/ts/process/templates/chatTemplate.ts @@ -3,6 +3,7 @@ import type { OpenAIChat } from '..'; import { get } from 'svelte/store'; import { DataBase } from 'src/ts/storage/database'; import { CurrentCharacter } from 'src/ts/stores'; +import { getUserName } from 'src/ts/util'; export const chatTemplates = { 'llama3': "{% set bos_token = '<|begin_of_text|>' %}{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{{ '<|start_header_id|>assistant<|end_header_id|>\n\n' }}", @@ -93,6 +94,6 @@ export const applyChatTemplate = (messages:OpenAIChat[]) => { "messages": formatedMessages, "add_generation_prompt": true, "risu_char": currentChar.name, - "risu_user": db.username + "risu_user": getUserName() }) } \ No newline at end of file diff --git a/src/ts/process/triggers.ts b/src/ts/process/triggers.ts index 5c635cbf..247b6470 100644 --- a/src/ts/process/triggers.ts +++ b/src/ts/process/triggers.ts @@ -1,4 +1,4 @@ -import { risuChatParser } from "../parser"; +import { parseChatML, risuChatParser, risuCommandParser } from "../parser"; import { DataBase, type Chat, type character } from "../storage/database"; import { tokenize } from "../tokenizer"; import { getModuleTriggers } from "./modules"; @@ -12,6 +12,8 @@ import { HypaProcesser } from "./memory/hypamemory"; import { requestChatData } from "./request"; import { generateAIImage } from "./stableDiff"; import { writeInlayImage } from "./files/image"; +import { runLua } from "./lua"; + export interface triggerscript{ comment: string; @@ -23,7 +25,7 @@ export interface triggerscript{ export type triggerCondition = triggerConditionsVar|triggerConditionsExists|triggerConditionsChatIndex -export type triggerEffect = triggerEffectCutChat|triggerEffectModifyChat|triggerEffectImgGen|triggerEffectRegex|triggerEffectRunLLM|triggerEffectCheckSimilarity|triggerEffectSendAIprompt|triggerEffectShowAlert|triggerEffectSetvar|triggerEffectSystemPrompt|triggerEffectImpersonate|triggerEffectCommand|triggerEffectStop|triggerEffectRunTrigger +export type triggerEffect = triggerCode|triggerEffectCutChat|triggerEffectModifyChat|triggerEffectImgGen|triggerEffectRegex|triggerEffectRunLLM|triggerEffectCheckSimilarity|triggerEffectSendAIprompt|triggerEffectShowAlert|triggerEffectSetvar|triggerEffectSystemPrompt|triggerEffectImpersonate|triggerEffectCommand|triggerEffectStop|triggerEffectRunTrigger export type triggerConditionsVar = { type:'var'|'value' @@ -32,6 +34,11 @@ export type triggerConditionsVar = { operator:'='|'!='|'>'|'<'|'>='|'<='|'null'|'true' } +export type triggerCode = { + type: 'triggercode'|'triggerlua', + code: string +} + export type triggerConditionsChatIndex = { type:'chatindex' value:string @@ -197,7 +204,10 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{ for(const trigger of triggers){ - if(arg.manualName){ + if(trigger.effect[0]?.type === 'triggercode' || trigger.effect[0]?.type === 'triggerlua'){ + // + } + else if(arg.manualName){ if(trigger.comment !== arg.manualName){ continue } @@ -238,22 +248,22 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{ } break case '>': - if(Number(varValue) > Number(conditionValue)){ + if(Number(varValue) <= Number(conditionValue)){ pass = false } break case '<': - if(Number(varValue) < Number(conditionValue)){ - pass = false - } - break - case '>=': if(Number(varValue) >= Number(conditionValue)){ pass = false } break + case '>=': + if(Number(varValue) < Number(conditionValue)){ + pass = false + } + break case '<=': - if(Number(varValue) <= Number(conditionValue)){ + if(Number(varValue) > Number(conditionValue)){ pass = false } break @@ -393,6 +403,7 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{ case 'input':{ const val = await alertInput(effectValue) setVar(inputVar, val) + break; } case 'select':{ const val = await alertSelect(effectValue.split('§')) @@ -416,41 +427,10 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{ } const effectValue = risuChatParser(effect.value,{chara:char}) const varName = effect.inputVar - let promptbody:OpenAIChat[] = [] - let currentRole:'user'|'assistant'|'system' - - const splited = effectValue.split('\n') - - for(let i = 0; i < splited.length; i++){ - const line = splited[i] - if(line.startsWith('@@role ')){ - const role = line.split(' ')[1] - switch(role){ - case 'user': - case 'assistant': - case 'system': - currentRole = role - break - default: - currentRole = 'system' - break - } - promptbody.push({role: currentRole, content: ''}) - continue - } - else if(promptbody.length === 0){ - promptbody.push({role: 'system', content: line}) - } - else{ - promptbody[promptbody.length - 1].content += line - } + let promptbody:OpenAIChat[] = parseChatML(effectValue) + if(!promptbody){ + promptbody = [{role:'user', content:effectValue}] } - - promptbody = promptbody.map((e) => { - e.content = e.content.trim() - return e - }).filter((e) => e.content.length > 0) - const result = await requestChatData({ formated: promptbody, bias: {}, @@ -518,6 +498,35 @@ export async function runTrigger(char:character,mode:triggerMode, arg:{ setVar(effect.inputVar, res) break } + + case 'triggercode':{ + const triggerCodeResult = await risuCommandParser(effect.code,{ + chara:char, + lowLevelAccess: trigger.lowLevelAccess, + funcName: mode + }) + + if(triggerCodeResult['__stop_chat__'] === '1'){ + stopSending = true + } + break + } + case 'triggerlua':{ + const triggerCodeResult = await runLua(effect.code,{ + lowLevelAccess: trigger.lowLevelAccess, + mode: mode === 'manual' ? arg.manualName : mode, + setVar: setVar, + getVar: getVar, + char: char, + chat: chat, + }) + + if(triggerCodeResult.stopSending){ + stopSending = true + } + chat = triggerCodeResult.chat + break + } } } } diff --git a/src/ts/storage/accountStorage.ts b/src/ts/storage/accountStorage.ts index 9a96dc14..41d1fd00 100644 --- a/src/ts/storage/accountStorage.ts +++ b/src/ts/storage/accountStorage.ts @@ -1,4 +1,4 @@ -import { get } from "svelte/store" +import { get, writable } from "svelte/store" import { DataBase } from "./database" import { hubURL } from "../characterCards" import localforage from "localforage" @@ -7,6 +7,10 @@ import { forageStorage, getUnpargeables, replaceDbResources } from "./globalApi" import { encodeRisuSave } from "./risuSave" import { v4 } from "uuid" +export const AccountWarning = writable('') + +let seenWarnings:string[] = [] + export class AccountStorage{ auth:string usingSync:boolean @@ -25,10 +29,23 @@ export class AccountStorage{ 'X-Format': 'nocheck' } }) + if(da.headers.get('Content-Type') === 'application/json'){ + const json = (await da.json()) + if(json?.warning){ + if(!seenWarnings.includes(json.warning)){ + seenWarnings.push(json.warning) + AccountWarning.set(json.warning) + } + } + } + if(da.status === 304){ return key } if(da.status === 403){ + if(da.headers.get('x-risu-status') === 'warn'){ + return + } localStorage.setItem("fallbackRisuToken",await alertLogin()) this.checkAuth() } diff --git a/src/ts/storage/database.ts b/src/ts/storage/database.ts index fcd6e252..b4aa3ec6 100644 --- a/src/ts/storage/database.ts +++ b/src/ts/storage/database.ts @@ -1,3 +1,8 @@ +export const DataBase = writable({} as any as Database) +export const loadedStore = writable(false) +export let appVer = "122.1.2" +export let webAppSubVer = '' + import { get, writable } from 'svelte/store'; import { checkNullish, decryptBuffer, encryptBuffer, selectSingleFile } from '../util'; import { changeLanguage, language } from '../../lang'; @@ -12,11 +17,6 @@ import { defaultColorScheme, type ColorScheme } from '../gui/colorscheme'; import type { PromptItem, PromptSettings } from '../process/prompt'; import type { OobaChatCompletionRequestParams } from '../model/ooba'; -export const DataBase = writable({} as any as Database) -export const loadedStore = writable(false) -export let appVer = "115.0.1" -export let webAppSubVer = '' - export function setDatabase(data:Database){ if(checkNullish(data.characters)){ data.characters = [] @@ -419,6 +419,16 @@ export function setDatabase(data:Database){ data.stabilityModel ??= 'sd3-large' data.stabllityStyle ??= '' data.legacyTranslation ??= false + data.comfyUiUrl ??= 'http://localhost:8188' + data.comfyConfig ??= { + workflow: '', + posNodeID: '', + posInputName: 'text', + negNodeID: '', + negInputName: 'text', + timeout: 30 + } + changeLanguage(data.language) DataBase.set(data) } @@ -585,6 +595,7 @@ export interface Database{ name:string icon:string largePortrait?:boolean + id?:string }[] assetWidth:number animationSpeed:number @@ -695,6 +706,8 @@ export interface Database{ stabilityKey: string stabllityStyle: string legacyTranslation: boolean + comfyConfig: ComfyConfig + comfyUiUrl: string } export interface customscript{ @@ -826,6 +839,7 @@ export interface character{ }> defaultVariables?:string lowLevelAccess?:boolean + hideChatIcon?:boolean } @@ -873,6 +887,7 @@ export interface groupChat{ nickname?:string defaultVariables?:string lowLevelAccess?:boolean + hideChatIcon?:boolean } export interface botPreset{ @@ -965,6 +980,16 @@ interface NAIImgConfig{ InfoExtracted:number, RefStrength:number } + +interface ComfyConfig{ + workflow:string, + posNodeID: string, + posInputName:string, + negNodeID: string, + negInputName:string, + timeout: number +} + export type FormatingOrderItem = 'main'|'jailbreak'|'chats'|'lorebook'|'globalNote'|'authorNote'|'lastChat'|'description'|'postEverything'|'personaPrompt' export interface Chat{ @@ -981,6 +1006,7 @@ export interface Chat{ scriptstate?:{[key:string]:string|number|boolean} modules?:string[] id?:string + bindedPersona?:string } export interface Message{ @@ -1299,9 +1325,9 @@ export async function downloadPreset(id:number, type:'json'|'risupreset'|'return } else if(type === 'risupreset' || type === 'return'){ const buf = fflate.compressSync(encodeMsgpack({ - presetVersion: 0, + presetVersion: 2, type: 'preset', - pres: await encryptBuffer( + preset: await encryptBuffer( encodeMsgpack(pres), 'risupreset' ) @@ -1342,8 +1368,8 @@ export async function importPreset(f:{ if(f.name.endsWith('.risupreset')){ const decoded = await decodeMsgpack(fflate.decompressSync(f.data)) console.log(decoded) - if(decoded.presetVersion === 0 && decoded.type === 'preset'){ - pre = {...presetTemplate,...decodeMsgpack(Buffer.from(await decryptBuffer(decoded.pres, 'risupreset')))} + if((decoded.presetVersion === 0 || decoded.presetVersion === 2) && decoded.type === 'preset'){ + pre = {...presetTemplate,...decodeMsgpack(Buffer.from(await decryptBuffer(decoded.preset ?? decoded.pres, 'risupreset')))} } } else{ @@ -1484,4 +1510,4 @@ export async function importPreset(f:{ pre.name ??= "Imported" db.botPresets.push(pre) setDatabase(db) -} \ No newline at end of file +} diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts index 5db4961c..95093e42 100644 --- a/src/ts/storage/globalApi.ts +++ b/src/ts/storage/globalApi.ts @@ -1,4 +1,5 @@ import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs" + import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "../util" import { convertFileSrc, invoke } from "@tauri-apps/api/tauri" import { v4 as uuidv4, v4 } from 'uuid'; @@ -11,7 +12,7 @@ import { checkRisuUpdate } from "../update"; import { botMakerMode, selectedCharID } from "../stores"; import { Body, ResponseType, fetch as TauriFetch } from "@tauri-apps/api/http"; import { loadPlugins } from "../plugins/plugins"; -import { alertConfirm, alertError, alertNormal, alertNormalWait } from "../alert"; +import { alertConfirm, alertError, alertNormal, alertNormalWait, alertSelect } from "../alert"; import { checkDriverInit, syncDrive } from "../drive/drive"; import { hasher } from "../parser"; import { characterURLImport, hubURL } from "../characterCards"; @@ -55,12 +56,26 @@ interface fetchLog{ let fetchLog:fetchLog[] = [] -/** - * Downloads a file with the given name and data. - * - * @param {string} name - The name of the file to be downloaded. - * @param {Uint8Array|ArrayBuffer|string} dat - The data of the file to be downloaded. - */ +async function writeBinaryFileFast(appPath: string, data: Uint8Array) { + const secret = await invoke('get_http_secret') as string; + const port = await invoke('get_http_port') as number; + + const apiUrl = `http://127.0.0.1:${port}/?path=${encodeURIComponent(appPath)}`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + 'x-tauri-secret': secret + }, + body: new Blob([data]) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } +} + export async function downloadFile(name:string, dat:Uint8Array|ArrayBuffer|string) { if(typeof(dat) === 'string'){ dat = Buffer.from(dat, 'utf-8') @@ -266,7 +281,7 @@ export async function saveAsset(data:Uint8Array, customId:string = '', fileName: fileExtension = fileName.split('.').pop() } if(isTauri){ - await writeBinaryFile(`assets/${id}.${fileExtension}`, data ,{dir: BaseDirectory.AppData}) + await writeBinaryFileFast(`assets/${id}.${fileExtension}`, data); return `assets/${id}.${fileExtension}` } else{ @@ -341,13 +356,14 @@ export async function saveDb(){ changed = false let db = get(DataBase) db.saveTime = Math.floor(Date.now() / 1000) - const dbData = encodeRisuSave(db) if(isTauri){ - await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData}) - await writeBinaryFile(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData, {dir: BaseDirectory.AppData}) + const dbData = encodeRisuSave(db) + await writeBinaryFileFast('database/database.bin', dbData); + await writeBinaryFileFast(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData); } else{ if(!forageStorage.isAccount){ + const dbData = encodeRisuSave(db) await forageStorage.setItem('database/database.bin', dbData) await forageStorage.setItem(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData) } @@ -386,7 +402,7 @@ export async function saveDb(){ async function getDbBackups() { let db = get(DataBase) if(db?.account?.useSync){ - return + return [] } if(isTauri){ const keys = await readDir('database', {dir: BaseDirectory.AppData}) @@ -446,14 +462,11 @@ export async function loadData() { await createDir('assets', {dir: BaseDirectory.AppData}) } if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){ - await writeBinaryFile('database/database.bin', - encodeRisuSave({}) - ,{dir: BaseDirectory.AppData}) + await writeBinaryFileFast('database/database.bin', encodeRisuSave({})); } try { - setDatabase( - decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData})) - ) + const decoded = decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData})) + setDatabase(decoded) } catch (error) { const backups = await getDbBackups() let backupLoaded = false @@ -483,10 +496,11 @@ export async function loadData() { await forageStorage.setItem('database/database.bin', gotStorage) } try { - setDatabase( - decodeRisuSave(gotStorage) - ) + const decoded = decodeRisuSave(gotStorage) + console.log(decoded) + setDatabase(decoded) } catch (error) { + console.error(error) const backups = await getDbBackups() let backupLoaded = false for(const backup of backups){ @@ -1076,23 +1090,24 @@ async function checkNewFormat(): Promise { if (v.lorebook) { v.lorebook = updateLorebooks(v.lorebook); } - return v; - }); + return v + }) - if (!db.formatversion) { - /** - * Checks and updates the path of a given data string. - * - * @param {string} data - The data string to be checked and updated. - * @returns {string} - The updated data string with the correct path. - */ - function checkParge(data: string): string { - if (data.startsWith('assets') || (data.length < 3)) { - return data; - } else { - const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1]); - if (!d) { - return data; + db.personas = (db.personas ?? []).map((v) => { + v.id ??= uuidv4() + return v + }) + + if(!db.formatversion){ + function checkParge(data:string){ + + if(data.startsWith('assets') || (data.length < 3)){ + return data + } + else{ + const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1]) + if(!d){ + return data } return d; } @@ -2021,4 +2036,47 @@ export class BlankWriter{ async end(){ //do nothing, just to make compatible with other writer } +} + +export async function loadInternalBackup(){ + + const keys = isTauri ? (await readDir('database', {dir: BaseDirectory.AppData})).map((v) => { + return v.name + }) : (await forageStorage.keys()) + let internalBackups:string[] = [] + for(const key of keys){ + if(key.includes('dbbackup-')){ + internalBackups.push(key) + } + } + + const selectOptions = [ + 'Cancel', + ...(internalBackups.map((a) => { + return (new Date(parseInt(a.replace('database/dbbackup-', '').replace('dbbackup-','')) * 100)).toLocaleString() + })) + ] + + const alertResult = parseInt( + await alertSelect(selectOptions) + ) - 1 + + if(alertResult === -1){ + return + } + + const selectedBackup = internalBackups[alertResult] + + const data = isTauri ? ( + await readBinaryFile('database/' + selectedBackup, {dir: BaseDirectory.AppData}) + ) : (await forageStorage.getItem(selectedBackup)) + + setDatabase( + decodeRisuSave(data) + ) + + await alertNormal('Loaded backup') + + + } \ No newline at end of file diff --git a/src/ts/storage/risuSave.ts b/src/ts/storage/risuSave.ts index 472bedbb..255f0c9d 100644 --- a/src/ts/storage/risuSave.ts +++ b/src/ts/storage/risuSave.ts @@ -16,7 +16,7 @@ const magicCompressedHeader = new Uint8Array([0, 82, 73, 83, 85, 83, 65, 86, 69, export function encodeRisuSave(data:any, compression:'noCompression'|'compression' = 'noCompression'){ let encoded:Uint8Array = packr.encode(data) - if(isTauri || compression === 'compression'){ + if(compression === 'compression'){ encoded = fflate.compressSync(encoded) const result = new Uint8Array(encoded.length + magicCompressedHeader.length); result.set(magicCompressedHeader, 0) diff --git a/src/ts/stores.ts b/src/ts/stores.ts index 7809fa19..2b3a6f2d 100644 --- a/src/ts/stores.ts +++ b/src/ts/stores.ts @@ -1,7 +1,9 @@ -import { get, writable } from "svelte/store"; -import { DataBase, type character, type groupChat } from "./storage/database"; +import { get, writable, type Writable } from "svelte/store"; +import { DataBase, type Chat, type character, type groupChat } from "./storage/database"; import { isEqual } from "lodash"; import type { simpleCharacterArgument } from "./parser"; +import { getUserIcon, getUserIconProtrait, getUserName, sleep } from "./util"; +import { getModules } from "./process/modules"; function updateSize(){ SizeStore.set({ @@ -23,25 +25,29 @@ export const CharEmotion = writable({} as {[key:string]: [string, string, number export const ViewBoxsize = writable({ width: 12 * 16, height: 12 * 16 }); // Default width and height in pixels export const settingsOpen = writable(false) export const botMakerMode = writable(false) +export const moduleBackgroundEmbedding = writable('') //optimization -let db = get(DataBase) -let currentChar = get(selectedCharID) -let currentCharacter = db.characters ? (db.characters[currentChar]) : null -let currentChat = currentCharacter ? (currentCharacter.chats[currentCharacter.chatPage]) : null -export const CurrentCharacter = writable(structuredClone(currentCharacter)) -export const CurrentSimpleCharacter = writable(createSimpleCharacter(currentCharacter)) -export const CurrentChat = writable(structuredClone(currentChat)) -export const CurrentUsername = writable(db.username) -export const CurrentUserIcon = writable(db.userIcon) -export const CurrentShowMemoryLimit = writable(db.showMemoryLimit) +export const CurrentCharacter = writable(null) as Writable +export const CurrentSimpleCharacter = writable(null) as Writable +export const CurrentChat = writable(null) as Writable +export const CurrentUsername = writable('') as Writable +export const CurrentUserIcon = writable('') as Writable +export const CurrentShowMemoryLimit = writable(false) as Writable export const ShowVN = writable(false) export const SettingsMenuIndex = writable(-1) export const CurrentVariablePointer = writable({} as {[key:string]: string|number|boolean}) +export const ReloadGUIPointer = writable(0) export const OpenRealmStore = writable(false) export const ShowRealmFrameStore = writable('') export const PlaygroundStore = writable(0) +export const HideIconStore = writable(false) +export const UserIconProtrait = writable(false) +let lastGlobalEnabledModules: string[] = [] +let lastChatEnabledModules: string[] = [] +let moduleHideIcon = false +let characterHideIcon = false function createSimpleCharacter(char:character|groupChat){ if((!char) || char.type === 'group'){ @@ -55,107 +61,196 @@ function createSimpleCharacter(char:character|groupChat){ additionalAssets: char.additionalAssets, virtualscript: char.virtualscript, emotionImages: char.emotionImages, + triggerscript: char.triggerscript, } return simpleChar } - -function updateCurrentCharacter(){ - - const db = get(DataBase) - if(!db.characters){ - CurrentCharacter.set(null) - updateCurrentChat() - return - } - - const currentCharId = get(selectedCharID) - const currentChar = db.characters[currentCharId] - const gotCharacter = get(CurrentCharacter) - if(isEqual(gotCharacter, currentChar)){ - return - } - if((currentChar?.viewScreen === 'vn') !== get(ShowVN)){ - ShowVN.set(currentChar?.viewScreen === 'vn') - } - - CurrentCharacter.set(structuredClone(currentChar)) - const simp = createSimpleCharacter(currentChar) - - if(!isEqual(get(CurrentSimpleCharacter), simp)){ - CurrentSimpleCharacter.set(simp) - } - - updateCurrentChat() +function trySync(){ + try { + let db = get(DataBase) + let currentChar = get(selectedCharID) + let currentCharacter = db.characters ? (db.characters[currentChar]) : null + let currentChat = currentCharacter ? (currentCharacter.chats[currentCharacter.chatPage]) : null + CurrentCharacter.set(structuredClone(currentCharacter)) + CurrentSimpleCharacter.set(createSimpleCharacter(currentCharacter)) + CurrentChat.set(structuredClone(currentChat)) + CurrentUsername.set(getUserName()) + CurrentUserIcon.set(getUserIcon()) + CurrentShowMemoryLimit.set(db.showMemoryLimit) + } catch (error) {} } -function updateCurrentChat(){ - const currentChar = get(CurrentCharacter) - if(!currentChar){ - CurrentChat.set(null) - return - } - const chat = (currentChar.chats[currentChar.chatPage]) - const gotChat = get(CurrentChat) - if(isEqual(gotChat, chat)){ - return - } - CurrentChat.set(structuredClone(chat)) -} +trySync() -DataBase.subscribe((data) => { - updateCurrentCharacter() - if(data.username !== get(CurrentUsername)){ - CurrentUsername.set(data.username) - } - if(data.userIcon !== get(CurrentUserIcon)){ - CurrentUserIcon.set(data.userIcon) - } - if(data.showMemoryLimit !== get(CurrentShowMemoryLimit)){ - CurrentShowMemoryLimit.set(data.showMemoryLimit) - } -}) - -selectedCharID.subscribe((id) => { - - updateCurrentCharacter() -}) - -CurrentCharacter.subscribe((char) => { - updateCurrentChat() - let db = get(DataBase) - let charId = get(selectedCharID) - if(charId === -1 || charId > db.characters.length){ - return - } - let cha = db.characters[charId] - if(isEqual(cha, char)){ - return - } - db.characters[charId] = structuredClone(char) - DataBase.set(db) -}) - -CurrentChat.subscribe((chat) => { - let currentChar = get(CurrentCharacter) - - if(currentChar){ - if(!isEqual(currentChar.chats[currentChar.chatPage], chat)){ - currentChar.chats[currentChar.chatPage] = structuredClone(chat) - CurrentCharacter.set(currentChar) +async function preInit(){ + await sleep(1) + trySync() + function updateCurrentCharacter(){ + + const db = get(DataBase) + if(!db.characters){ + CurrentCharacter.set(null) + updateCurrentChat() + return } + + const currentCharId = get(selectedCharID) + const currentChar = db.characters[currentCharId] + const gotCharacter = get(CurrentCharacter) + if(isEqual(gotCharacter, currentChar)){ + return + } + if((currentChar?.viewScreen === 'vn') !== get(ShowVN)){ + ShowVN.set(currentChar?.viewScreen === 'vn') + } + + CurrentCharacter.set(structuredClone(currentChar)) + const simp = createSimpleCharacter(currentChar) + + if(!isEqual(get(CurrentSimpleCharacter), simp)){ + CurrentSimpleCharacter.set(simp) + } + + updateCurrentChat() } - const variablePointer = get(CurrentVariablePointer) - const currentState = structuredClone(chat?.scriptstate) - - if(!isEqual(variablePointer, currentState)){ - CurrentVariablePointer.set(currentState) + function updateCurrentChat(){ + const currentChar = get(CurrentCharacter) + if(!currentChar){ + CurrentChat.set(null) + return + } + const chat = (currentChar.chats[currentChar.chatPage]) + const gotChat = get(CurrentChat) + if(isEqual(gotChat, chat)){ + return + } + CurrentChat.set(structuredClone(chat)) } -}) + DataBase.subscribe((data) => { + updateCurrentCharacter() + if(getUserName() !== get(CurrentUsername)){ + CurrentUsername.set(getUserName()) + } + if(getUserIcon() !== get(CurrentUserIcon)){ + CurrentUserIcon.set(getUserIcon()) + } + if(getUserIconProtrait() !== get(UserIconProtrait)){ + UserIconProtrait.set(getUserIconProtrait()) + } + if(data.showMemoryLimit !== get(CurrentShowMemoryLimit)){ + CurrentShowMemoryLimit.set(data.showMemoryLimit) + } + if(!isEqual(data.enabledModules, lastGlobalEnabledModules)){ + lastGlobalEnabledModules = data.enabledModules || [] + onModuleUpdate() + return + } + }) + + selectedCharID.subscribe((id) => { + + updateCurrentCharacter() + }) + + CurrentCharacter.subscribe((char) => { + updateCurrentChat() + let db = get(DataBase) + let charId = get(selectedCharID) + if(char?.hideChatIcon !== characterHideIcon){ + characterHideIcon = char?.hideChatIcon + HideIconStore.set(characterHideIcon || moduleHideIcon) + } + if(getUserName() !== get(CurrentUsername)){ + CurrentUsername.set(getUserName()) + } + if(getUserIcon() !== get(CurrentUserIcon)){ + CurrentUserIcon.set(getUserIcon()) + } + if(getUserIconProtrait() !== get(UserIconProtrait)){ + UserIconProtrait.set(getUserIconProtrait()) + } + if(charId === -1 || charId > db.characters.length){ + return + } + let cha = db.characters[charId] + if(isEqual(cha, char)){ + return + } + db.characters[charId] = structuredClone(char) + DataBase.set(db) + }) + + CurrentChat.subscribe((chat) => { + let currentChar = get(CurrentCharacter) + + if(currentChar){ + if(!isEqual(currentChar.chats[currentChar.chatPage], chat)){ + currentChar.chats[currentChar.chatPage] = structuredClone(chat) + CurrentCharacter.set(currentChar) + } + } + + if(!isEqual(lastChatEnabledModules, chat?.modules)){ + lastChatEnabledModules = chat?.modules || [] + onModuleUpdate() + return + } + + if(getUserName() !== get(CurrentUsername)){ + CurrentUsername.set(getUserName()) + } + + if(getUserIcon() !== get(CurrentUserIcon)){ + CurrentUserIcon.set(getUserIcon()) + } + + const variablePointer = get(CurrentVariablePointer) + const currentState = structuredClone(chat?.scriptstate) + + if(!isEqual(variablePointer, currentState)){ + CurrentVariablePointer.set(currentState) + } + }) +} + +function onModuleUpdate(){ + if(!Array.isArray(lastGlobalEnabledModules)){ + lastGlobalEnabledModules = [] + } + if(!Array.isArray(lastChatEnabledModules)){ + lastChatEnabledModules = [] + } + + const m = getModules([ + ...lastGlobalEnabledModules, ...lastChatEnabledModules + ]) + + let moduleHideIcon = false + let backgroundEmbedding = '' + m.forEach((module) => { + if(!module){ + return + } + + if(module.hideIcon){ + moduleHideIcon = true + } + if(module.backgroundEmbedding){ + backgroundEmbedding += '\n' + module.backgroundEmbedding + '\n' + } + }) + + if(backgroundEmbedding){ + moduleBackgroundEmbedding.set(backgroundEmbedding) + } + HideIconStore.set(characterHideIcon || moduleHideIcon) +} updateSize() -window.addEventListener("resize", updateSize); \ No newline at end of file +window.addEventListener("resize", updateSize); +preInit() \ No newline at end of file diff --git a/src/ts/textsynt.ts b/src/ts/textsynt.ts deleted file mode 100644 index 9010403e..00000000 --- a/src/ts/textsynt.ts +++ /dev/null @@ -1,33 +0,0 @@ - -interface TextSyntSyntaxTree { - name: string; - children: TextSyntSyntaxTree[]; -} - -function parseMarkdownLikeYaml(text:string){ - - const lines = text.split('\n'); - const root: TextSyntSyntaxTree = {name: 'root', children: []}; - let rootHashIndentation = -1; - let currentIndentation = 0; - - for(let i = 0; i < lines.length; i++){ - const line = lines[i]; - - if(line.startsWith('#')){ - let indentations = 0; - while(line[indentations] === '#'){ - indentations++; - } - - if(currentIndentation === 0 && rootHashIndentation === -1){ - rootHashIndentation = indentations; - indentations = 1; - } - else{ - indentations -= (rootHashIndentation - 1) - } - } - - } -} \ No newline at end of file diff --git a/src/ts/tokenizer.ts b/src/ts/tokenizer.ts index 15847575..7afe4fa0 100644 --- a/src/ts/tokenizer.ts +++ b/src/ts/tokenizer.ts @@ -17,6 +17,7 @@ export const tokenizerList = [ ['llama', 'Llama'], ['llama3', 'Llama3'], ['novellist', 'Novellist'], + ['gemma', 'Gemma'], ] as const export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Array)>{ @@ -35,6 +36,8 @@ export async function encode(data:string):Promise<(number[]|Uint32Array|Int32Arr return await tokenizeWebTokenizers(data, 'novellist') case 'llama3': return await tokenizeWebTokenizers(data, 'llama') + case 'gemma': + return await tokenizeWebTokenizers(data, 'gemma') default: // Add exception for gpt-4o tokenizers on reverse_proxy if(db.proxyRequestModel?.startsWith('gpt4o') || diff --git a/src/ts/translator/translator.ts b/src/ts/translator/translator.ts index 861a5f6b..832a5fc8 100644 --- a/src/ts/translator/translator.ts +++ b/src/ts/translator/translator.ts @@ -4,11 +4,11 @@ import { DataBase, type character, type customscript, type groupChat } from "../ import { globalFetch, isTauri } from "../storage/globalApi" import { alertError } from "../alert" import { requestChatData } from "../process/request" -import { doingChat } from "../process" -import type { simpleCharacterArgument } from "../parser" +import { doingChat, type OpenAIChat } from "../process" +import { applyMarkdownToNode, parseChatML, type simpleCharacterArgument } from "../parser" import { selectedCharID } from "../stores" import { getModuleRegexScripts } from "../process/modules" -import { getNodetextToSentence, sleep, applyMarkdownToNode } from "../util" +import { getNodetextToSentence, sleep } from "../util" import { processScriptFull } from "../process/scripts" import { Capacitor } from "@capacitor/core" @@ -448,11 +448,23 @@ async function translateLLM(text:string, arg:{to:string}){ if(llmCache.has(text)){ return llmCache.get(text) } + const styleDecodeRegex = /\(.+?)\<\/risu-style\>/gms + let styleDecodes:string[] = [] + text = text.replace(styleDecodeRegex, (match, p1) => { + styleDecodes.push(p1) + return `` + }) + const db = get(DataBase) + let formated:OpenAIChat[] = [] let prompt = db.translatorPrompt || `You are a translator. translate the following html or text into {{slot}}. do not output anything other than the translation.` - prompt = prompt.replace('{{slot}}', arg.to) - const rq = await requestChatData({ - formated: [ + let parsedPrompt = parseChatML(prompt.replaceAll('{{slot}}', arg.to).replaceAll('{{solt::content}}', text)) + if(parsedPrompt){ + formated = parsedPrompt + } + else{ + prompt = prompt.replaceAll('{{slot}}', arg.to) + formated = [ { 'role': 'system', 'content': prompt @@ -461,7 +473,10 @@ async function translateLLM(text:string, arg:{to:string}){ 'role': 'user', 'content': text } - ], + ] + } + const rq = await requestChatData({ + formated, bias: {}, useStreaming: false, noMultiGen: true, @@ -472,6 +487,9 @@ async function translateLLM(text:string, arg:{to:string}){ alertError(`${rq.result}`) return text } - llmCache.set(text, rq.result) - return rq.result + const result = rq.result.replace(//g, (match, p1) => { + return styleDecodes[parseInt(p1)] ?? '' + }).replace(/<\/style-data>/g, '') + llmCache.set(text, result) + return result } \ No newline at end of file diff --git a/src/ts/util.ts b/src/ts/util.ts index 7dccb2f8..e1d25be8 100644 --- a/src/ts/util.ts +++ b/src/ts/util.ts @@ -8,19 +8,9 @@ import { basename } from "@tauri-apps/api/path" import { createBlankChar, getCharImage } from "./characters" import { appWindow } from '@tauri-apps/api/window'; import { isTauri } from "./storage/globalApi" -import { Marked } from "marked" export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 -const mconverted = new Marked({ - gfm: true, - breaks: true, - silent: true, - tokenizer: { - - } -}) - export interface Messagec extends Message{ index: number } @@ -113,10 +103,69 @@ export const replacePlaceholders = (msg:string, name:string) => { let selectedChar = get(selectedCharID) let currentChar = db.characters[selectedChar] return msg .replace(/({{char}})|({{Char}})|()|()/gi, currentChar.name) - .replace(/({{user}})|({{User}})|()|()/gi, db.username) + .replace(/({{user}})|({{User}})|()|()/gi, getUserName()) .replace(/(\{\{((set)|(get))var::.+?\}\})/gu,'') } +function checkPersonaBinded(){ + try { + let db = get(DataBase) + const selectedChar = get(selectedCharID) + const character = db.characters[selectedChar] + const chat = character.chats[character.chatPage] + console.log(chat.bindedPersona) + if(!chat.bindedPersona){ + return null + } + const persona = db.personas.find(v => v.id === chat.bindedPersona) + console.log(db.personas, persona) + return persona + } catch (error) { + return null + } +} + +export function getUserName(){ + const bindedPersona = checkPersonaBinded() + if(bindedPersona){ + return bindedPersona.name + } + const db = get(DataBase) + return db.username ?? 'User' +} + +export function getUserIcon(){ + const bindedPersona = checkPersonaBinded() + console.log(`Icon: ${bindedPersona?.icon}`) + if(bindedPersona){ + return bindedPersona.icon + } + const db = get(DataBase) + return db.userIcon ?? '' +} + +export function getPersonaPrompt(){ + const bindedPersona = checkPersonaBinded() + if(bindedPersona){ + return bindedPersona.personaPrompt + } + const db = get(DataBase) + return db.personaPrompt ?? '' +} + +export function getUserIconProtrait(){ + try { + const bindedPersona = checkPersonaBinded() + if(bindedPersona){ + return bindedPersona.largePortrait + } + const db = get(DataBase) + return db.personas[db.selectedPersona].largePortrait + } catch (error) { + return false + } +} + export function checkIsIos(){ return /(iPad|iPhone|iPod)/g.test(navigator.userAgent) } @@ -585,33 +634,6 @@ export function getNodetextToSentence(node: Node): string { return result; } -export function applyMarkdownToNode(node: Node) { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent; - if (text) { - let markdown = mconverted.parseInline(text); - if (markdown !== text) { - const span = document.createElement('span'); - span.innerHTML = markdown; - - // inherit inline style from the parent node - const parentStyle = (node.parentNode as HTMLElement)?.style; - if(parentStyle){ - for(let i=0;i { } export function parseKeyValue(template:string){ - if(!template){ + try { + if(!template){ + return [] + } + + const keyValue:[string, string][] = [] + + for(const line of template.split('\n')){ + const [key, value] = line.split('=') + if(key && value){ + keyValue.push([key, value]) + } + } + + return keyValue + } catch (error) { return [] } - - const keyValue:[string, string][] = [] - - for(const line of template.split('\n')){ - const [key, value] = line.split('=') - if(key && value){ - keyValue.push([key, value]) - } - } - - return keyValue } export const sortableOptions = { diff --git a/version.json b/version.json index 5f595766..1f9e1c43 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{"version":"115.0.1"} \ No newline at end of file +{"version":"122.1.2"} \ No newline at end of file