diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/README.md b/README.md index eaf4897..b33579a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Check package.json for commands, I can't be bothered. ### Changelog +- 2.0: Multimessage support! Tracks changes to character descriptions and scenarios across multiple extractions. Shows version badges, message counts, and provides detailed change history viewer. - 1.9: Not again. They changed stuff again. What is this? - 1.8: Handling for new prompt structure, new instructions - 1.7: Handling for nested XML character tags diff --git a/package-lock.json b/package-lock.json index d6da3d1..3bb8726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,17 +108,19 @@ } }, "node_modules/@next/env": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", - "integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==" + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.32.tgz", + "integrity": "sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==", + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", - "integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz", + "integrity": "sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -128,12 +130,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", - "integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz", + "integrity": "sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -143,12 +146,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", - "integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz", + "integrity": "sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -158,12 +162,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", - "integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz", + "integrity": "sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -173,12 +178,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", - "integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz", + "integrity": "sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -188,12 +194,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", - "integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz", + "integrity": "sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -203,12 +210,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", - "integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz", + "integrity": "sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -218,12 +226,13 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", - "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz", + "integrity": "sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==", "cpu": [ "ia32" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -233,12 +242,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", - "integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz", + "integrity": "sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -816,7 +826,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -856,12 +867,13 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -890,9 +902,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -951,6 +964,19 @@ "node": ">=10.16.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1056,6 +1082,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1135,6 +1162,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -1154,6 +1182,20 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1170,6 +1212,51 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1259,12 +1346,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1305,6 +1395,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -1313,6 +1427,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -1343,11 +1470,50 @@ "node": ">=10.13.0" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1503,6 +1669,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1527,6 +1702,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1535,6 +1711,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1592,11 +1769,12 @@ } }, "node_modules/next": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", - "integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", + "version": "14.2.32", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.32.tgz", + "integrity": "sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==", + "license": "MIT", "dependencies": { - "@next/env": "14.2.23", + "@next/env": "14.2.32", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -1611,15 +1789,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.23", - "@next/swc-darwin-x64": "14.2.23", - "@next/swc-linux-arm64-gnu": "14.2.23", - "@next/swc-linux-arm64-musl": "14.2.23", - "@next/swc-linux-x64-gnu": "14.2.23", - "@next/swc-linux-x64-musl": "14.2.23", - "@next/swc-win32-arm64-msvc": "14.2.23", - "@next/swc-win32-ia32-msvc": "14.2.23", - "@next/swc-win32-x64-msvc": "14.2.23" + "@next/swc-darwin-arm64": "14.2.32", + "@next/swc-darwin-x64": "14.2.32", + "@next/swc-linux-arm64-gnu": "14.2.32", + "@next/swc-linux-arm64-musl": "14.2.32", + "@next/swc-linux-x64-gnu": "14.2.32", + "@next/swc-linux-x64-musl": "14.2.32", + "@next/swc-win32-arm64-msvc": "14.2.32", + "@next/swc-win32-ia32-msvc": "14.2.32", + "@next/swc-win32-x64-msvc": "14.2.32" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index cdbfd2c..802e7e4 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -1,9 +1,32 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +interface CardVersion { + version: number; + timestamp: number; + changes: { + description?: { old: string; new: string }; + scenario?: { old: string; new: string }; + }; + changeType: 'initial' | 'update'; + messageCount: number; + addedText?: { + description?: string; + scenario?: string; + }; + removedText?: { + description?: string; + scenario?: string; + }; +} + interface StoredCard extends CardData { timestamp: number; id: string; + versions: CardVersion[]; + currentVersion: number; + messageCount: number; + conversationId: string; } let extractedCards: StoredCard[] = []; @@ -26,6 +49,7 @@ interface Message { interface CardData { name: string; + trackingName: string; first_mes: string; description: string; personality: string; @@ -61,12 +85,23 @@ function extractCardData(messages: Message[]): CardData { const nameContent = messages[3].content; const lastColonIndex = nameContent.lastIndexOf(': '); - const nameFromUser = lastColonIndex !== -1 ? nameContent.substring(lastColonIndex + 2) : ''; + const nameFromUser = lastColonIndex !== -1 ? nameContent.substring(lastColonIndex + 2).trim() : ''; let content = messages[0].content.replace(/{user}/g, '{{user}}'); const inferredName = extractPersonaName(content); content = removePersonaTags(content); - const name = nameFromUser === '.' && inferredName ? inferredName : nameFromUser; + + // Use inferred name for tracking, but keep user input for display + const trackingName = inferredName || nameFromUser || 'Unknown Character'; + let displayName = nameFromUser; + if (nameFromUser === '.' || nameFromUser === '') { + displayName = inferredName || 'Unknown Character'; + } + + // Clean up tracking name + const cleanTrackingName = trackingName.replace(/[^a-zA-Z0-9\s]/g, '').trim(); + + console.log(`Name extraction: user="${nameFromUser}", inferred="${inferredName}", tracking="${cleanTrackingName}", display="${displayName}"`); if (!content.includes('<.>') || !content.includes('.')) { throw new Error('Required substrings not found'); @@ -93,7 +128,8 @@ function extractCardData(messages: Message[]): CardData { const description = content.trim(); return { - name, + name: displayName, + trackingName: cleanTrackingName, first_mes, description, personality: '', @@ -102,6 +138,82 @@ function extractCardData(messages: Message[]): CardData { }; } +function generateConversationId(messages: Message[]): string { + // Create a simple hash from the character name in the persona tag to identify conversations + const content = messages[0]?.content || ''; + const personaMatch = content.match(/<([^<>]+?)\s*'s\s+Persona>/i); + if (personaMatch) { + return personaMatch[1].trim().toLowerCase().replace(/[^a-zA-Z0-9]/g, ''); + } + // Fallback to content-based ID + return content.substring(0, 50).replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); +} + +function detectChanges(newCard: CardData, existingCard: StoredCard): { description?: { old: string; new: string }; scenario?: { old: string; new: string } } | null { + const changes: { description?: { old: string; new: string }; scenario?: { old: string; new: string } } = {}; + let hasChanges = false; + + if (newCard.description.trim() !== existingCard.description.trim()) { + changes.description = { old: existingCard.description, new: newCard.description }; + hasChanges = true; + } + + if (newCard.scenario.trim() !== existingCard.scenario.trim()) { + changes.scenario = { old: existingCard.scenario, new: newCard.scenario }; + hasChanges = true; + } + + return hasChanges ? changes : null; +} + +function findExistingCard(trackingName: string): StoredCard | null { + // Find by tracking name (inferred character name) to group same characters + return extractedCards.find(card => card.trackingName === trackingName) || null; +} + +function updateCardWithVersion(existingCard: StoredCard, newCard: CardData, changes: { description?: { old: string; new: string }; scenario?: { old: string; new: string } }): void { + const addedText: { description?: string; scenario?: string } = {}; + const removedText: { description?: string; scenario?: string } = {}; + + // Extract only the different text + if (changes.description) { + const added = extractAddedText(changes.description.old, changes.description.new); + const removed = extractRemovedText(changes.description.old, changes.description.new); + if (added) addedText.description = added; + if (removed) removedText.description = removed; + } + + if (changes.scenario) { + const added = extractAddedText(changes.scenario.old, changes.scenario.new); + const removed = extractRemovedText(changes.scenario.old, changes.scenario.new); + if (added) addedText.scenario = added; + if (removed) removedText.scenario = removed; + } + + const newVersion: CardVersion = { + version: existingCard.currentVersion + 1, + timestamp: Date.now(), + changes, + changeType: 'update', + messageCount: existingCard.messageCount + 1, + addedText: Object.keys(addedText).length > 0 ? addedText : undefined, + removedText: Object.keys(removedText).length > 0 ? removedText : undefined + }; + + existingCard.versions.push(newVersion); + existingCard.currentVersion = newVersion.version; + existingCard.timestamp = Date.now(); + existingCard.messageCount += 1; + + // Update the main card data + if (changes.description) { + existingCard.description = changes.description.new; + } + if (changes.scenario) { + existingCard.scenario = changes.scenario.new; + } +} + export async function POST(request: NextRequest) { if (request.method === "OPTIONS") { return new NextResponse(null, { @@ -117,7 +229,14 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); + // Check if this is a streaming request (JanitorAI expects SSE) + const acceptHeader = request.headers.get('accept'); + const isStreamingRequest = acceptHeader?.includes('text/event-stream') || body.stream === true; + if (!body.messages || body.messages.length < 2) { + if (isStreamingRequest) { + return createSSEErrorResponse("Missing messages or insufficient message count"); + } return NextResponse.json( { error: "Missing messages or insufficient message count" }, { @@ -130,56 +249,243 @@ export async function POST(request: NextRequest) { } const cardData = extractCardData(body.messages); - extractedCards.push({ - ...cardData, - timestamp: Date.now(), - id: generateId(), - }); + const conversationId = generateConversationId(body.messages); + const existingCard = findExistingCard(cardData.trackingName); + + console.log(`Conversation ID: ${conversationId}`); + + let responseMessage = "Got it."; + let changesSummary = ""; + + console.log(`Processing card: "${cardData.name}" (tracking: "${cardData.trackingName}"), ConversationID: ${conversationId}`); + console.log(`Existing cards: ${extractedCards.map(c => `"${c.name}" (tracking: "${c.trackingName}", v${c.currentVersion})`).join(', ')}`); + console.log(`Found existing card: ${existingCard ? `YES - v${existingCard.currentVersion}` : 'NO'}`); + + if (existingCard) { + const changes = detectChanges(cardData, existingCard); + console.log(`Changes detected:`, changes ? 'YES' : 'NO'); + if (changes) { + console.log(`Updating from v${existingCard.currentVersion} to v${existingCard.currentVersion + 1}`); + updateCardWithVersion(existingCard, cardData, changes); + + // Keep the original display name (don't update it) + // existingCard.name stays the same + + // Create a summary of changes for the response + const changeTypes = []; + if (changes.description) changeTypes.push("description"); + if (changes.scenario) changeTypes.push("scenario"); + + changesSummary = ` Changes detected in ${changeTypes.join(" and ")}.`; + responseMessage = `Character updated (v${existingCard.currentVersion}).${changesSummary}`; + } else { + existingCard.messageCount += 1; + // Keep the original display name (don't update it) + // existingCard.name stays the same + responseMessage = `Character data unchanged (v${existingCard.currentVersion}, message ${existingCard.messageCount}).`; + } + } else { + // Create new card with initial version + const newCard: StoredCard = { + ...cardData, + timestamp: Date.now(), + id: generateId(), + conversationId, + messageCount: 1, + versions: [{ + version: 1, + timestamp: Date.now(), + changes: { + description: { old: "", new: cardData.description }, + scenario: { old: "", new: cardData.scenario } + }, + changeType: 'initial', + messageCount: 1 + }], + currentVersion: 1 + }; + extractedCards.push(newCard); + responseMessage = `New character "${cardData.trackingName}" created (v1).`; + } cleanupExpiredCards(); + // Return SSE response if requested, otherwise JSON + if (isStreamingRequest) { + return createSSEResponse(responseMessage); + } + + // Return proper OpenAI-compatible response return NextResponse.json( { + id: `chatcmpl-${generateId()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: "sucker-v2", choices: [{ + index: 0, message: { - content: "Got it." - } - }] + role: "assistant", + content: responseMessage + }, + finish_reason: "stop" + }], + usage: { + prompt_tokens: 0, + completion_tokens: responseMessage.split(' ').length, + total_tokens: responseMessage.split(' ').length + } }, { headers: { "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", }, } ); } catch (error) { console.error("Error processing request:", error); + const errorMessage = "You dingus, read the directions on sucker before trying again."; + + // Check if this was a streaming request + const acceptHeader = request.headers.get('accept'); + const isStreamingRequest = acceptHeader?.includes('text/event-stream'); + + if (isStreamingRequest) { + return createSSEErrorResponse(errorMessage); + } + return NextResponse.json( { + id: `chatcmpl-${generateId()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: "sucker-v2", choices: [{ + index: 0, message: { - content: "You dingus, read the directions on sucker before trying again." - } - }] + role: "assistant", + content: errorMessage + }, + finish_reason: "stop" + }], + usage: { + prompt_tokens: 0, + completion_tokens: errorMessage.split(' ').length, + total_tokens: errorMessage.split(' ').length + } }, { - status: 400, + status: 200, // Change to 200 so Janitor AI accepts it headers: { "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", }, } ); } } -export async function GET() { +function getInitialCardVersion(card: StoredCard): CardData { + // Get the initial version (v1) of the card + const initialVersion = card.versions.find(v => v.version === 1); + if (initialVersion && initialVersion.changes.description && initialVersion.changes.scenario) { + return { + name: card.name, + trackingName: card.trackingName, + first_mes: card.first_mes, + description: initialVersion.changes.description.new, + personality: card.personality, + mes_example: card.mes_example, + scenario: initialVersion.changes.scenario.new, + }; + } + // Fallback to current version if initial not found + return { + name: card.name, + trackingName: card.trackingName, + first_mes: card.first_mes, + description: card.description, + personality: card.personality, + mes_example: card.mes_example, + scenario: card.scenario, + }; +} + +export async function GET(request: NextRequest) { cleanupExpiredCards(); + const url = new URL(request.url); + const isChangesRequest = url.searchParams.get('changes') === 'true'; + const cardId = url.searchParams.get('cardId'); + + if (isChangesRequest && cardId) { + const card = extractedCards.find(c => c.id === cardId); + + if (!card || !card.versions) { + return NextResponse.json( + { error: "Card not found or no version history available" }, + { + status: 404, + headers: { + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + + const changesReport = { + cardName: card.name, + cardId: card.id, + totalVersions: card.versions.length, + currentVersion: card.currentVersion, + versions: card.versions.map((version, index) => { + const result: any = { + version: version.version, + timestamp: new Date(version.timestamp).toISOString(), + changeType: version.changeType, + changes: version.changes + }; + + // Add extracted text information + if (version.addedText) { + result.addedText = version.addedText; + } + if (version.removedText) { + result.removedText = version.removedText; + } + + return result; + }), + summary: generateChangesSummary(card.versions) + }; + + // Sanitize filename for download + const sanitizedName = card.name.replace(/[^a-zA-Z0-9\-_]/g, '_'); + + return NextResponse.json(changesReport, { + headers: { + "Access-Control-Allow-Origin": "*", + "Content-Disposition": `attachment; filename="${sanitizedName}_changes.json"`, + }, + }); + } + return NextResponse.json( { status: "online", - cards: extractedCards.map(({ timestamp, ...card }) => card), + cards: extractedCards.map((card) => { + const { timestamp, versions, ...cardData } = card; + const initialVersion = getInitialCardVersion(card); + return { + ...cardData, + hasVersions: versions && versions.length > 1, + versionCount: versions ? versions.length : 0, + messageCount: card.messageCount || 1, + initialVersion: initialVersion + }; + }), }, { headers: { @@ -189,6 +495,231 @@ export async function GET() { ); } +interface DiffResult { + type: 'added' | 'removed' | 'unchanged'; + text: string; +} + +function extractAddedText(oldText: string, newText: string): string { + // Split by double newlines to get paragraphs, then by single newlines to get lines + const oldParagraphs = oldText.split(/\n\s*\n/); + const newParagraphs = newText.split(/\n\s*\n/); + + const addedBlocks: string[] = []; + + // Find paragraphs that exist in new but not in old + for (const newPara of newParagraphs) { + const newParaTrimmed = newPara.trim(); + if (!newParaTrimmed) continue; + + // Check if this paragraph (or a very similar one) exists in old text + let found = false; + for (const oldPara of oldParagraphs) { + const oldParaTrimmed = oldPara.trim(); + if (!oldParaTrimmed) continue; + + // Check for exact match or high similarity (80% of words match) + if (oldParaTrimmed === newParaTrimmed || calculateSimilarity(oldParaTrimmed, newParaTrimmed) > 0.8) { + found = true; + break; + } + } + + if (!found) { + addedBlocks.push(newParaTrimmed); + } + } + + return addedBlocks.join('\n\n'); +} + +function extractRemovedText(oldText: string, newText: string): string { + // Split by double newlines to get paragraphs + const oldParagraphs = oldText.split(/\n\s*\n/); + const newParagraphs = newText.split(/\n\s*\n/); + + const removedBlocks: string[] = []; + + // Find paragraphs that exist in old but not in new + for (const oldPara of oldParagraphs) { + const oldParaTrimmed = oldPara.trim(); + if (!oldParaTrimmed) continue; + + // Check if this paragraph (or a very similar one) exists in new text + let found = false; + for (const newPara of newParagraphs) { + const newParaTrimmed = newPara.trim(); + if (!newParaTrimmed) continue; + + // Check for exact match or high similarity (80% of words match) + if (oldParaTrimmed === newParaTrimmed || calculateSimilarity(oldParaTrimmed, newParaTrimmed) > 0.8) { + found = true; + break; + } + } + + if (!found) { + removedBlocks.push(oldParaTrimmed); + } + } + + return removedBlocks.join('\n\n'); +} + +function calculateSimilarity(text1: string, text2: string): number { + const words1 = text1.toLowerCase().split(/\s+/); + const words2 = text2.toLowerCase().split(/\s+/); + + const set1 = new Set(words1); + const set2 = new Set(words2); + + const intersection = new Set([...set1].filter(x => set2.has(x))); + const union = new Set([...set1, ...set2]); + + return intersection.size / union.size; +} + +function generateChangesSummary(versions: CardVersion[]) { + const summary = { + descriptionChanges: 0, + scenarioChanges: 0, + totalMessages: 0, + firstChange: null as string | null, + lastChange: null as string | null, + }; + + versions.forEach(version => { + if (version.changes.description !== undefined) { + summary.descriptionChanges++; + } + if (version.changes.scenario !== undefined) { + summary.scenarioChanges++; + } + summary.totalMessages = Math.max(summary.totalMessages, version.messageCount || 0); + }); + + if (versions.length > 0) { + summary.firstChange = new Date(versions[0].timestamp).toISOString(); + summary.lastChange = new Date(versions[versions.length - 1].timestamp).toISOString(); + } + + return summary; +} + +// SSE Helper Functions +function createSSEResponse(content: string): Response { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + // Send the message in OpenAI streaming format + const id = `chatcmpl-${generateId()}`; + const timestamp = Math.floor(Date.now() / 1000); + + // Send initial chunk with message + const chunk = { + id, + object: "chat.completion.chunk", + created: timestamp, + model: "sucker-v2", + choices: [{ + index: 0, + delta: { + role: "assistant", + content: content + }, + finish_reason: null + }] + }; + + controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + + // Send final chunk to indicate completion + const finalChunk = { + id, + object: "chat.completion.chunk", + created: timestamp, + model: "sucker-v2", + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }] + }; + + controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)); + controller.enqueue(encoder.encode(`data: [DONE]\n\n`)); + controller.close(); + } + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS, GET', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} + +function createSSEErrorResponse(errorMessage: string): Response { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + const id = `chatcmpl-${generateId()}`; + const timestamp = Math.floor(Date.now() / 1000); + + // Send error as a normal message chunk + const chunk = { + id, + object: "chat.completion.chunk", + created: timestamp, + model: "sucker-v2", + choices: [{ + index: 0, + delta: { + role: "assistant", + content: errorMessage + }, + finish_reason: null + }] + }; + + controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + + // Send final chunk + const finalChunk = { + id, + object: "chat.completion.chunk", + created: timestamp, + model: "sucker-v2", + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }] + }; + + controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`)); + controller.enqueue(encoder.encode(`data: [DONE]\n\n`)); + controller.close(); + } + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS, GET', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); +} + export async function OPTIONS() { return new NextResponse(null, { status: 204, diff --git a/src/app/page.tsx b/src/app/page.tsx index 1c8599b..786dab0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -35,6 +35,17 @@ interface Card { mes_example: string; scenario: string; avatarUrl?: string; + hasVersions?: boolean; + versionCount?: number; + messageCount?: number; + initialVersion?: { + name: string; + first_mes: string; + description: string; + personality: string; + mes_example: string; + scenario: string; + }; } export default function Home() { @@ -48,6 +59,9 @@ export default function Home() { const [avatarPath, setAvatarPath] = useState(""); const [isMetadataOpen, setIsMetadataOpen] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); + const [changesDialogOpen, setChangesDialogOpen] = useState(false); + const [selectedChanges, setSelectedChanges] = useState(null); + const [showFullText, setShowFullText] = useState(false); const fetchCards = async () => { try { @@ -76,17 +90,74 @@ export default function Home() { }, []); const downloadJson = (card: Card) => { + // Use initial version for download, or current version if no initial version available + const downloadData = card.initialVersion ? { + name: card.initialVersion.name, + first_mes: card.initialVersion.first_mes, + description: card.initialVersion.description, + personality: card.initialVersion.personality, + mes_example: card.initialVersion.mes_example, + scenario: card.initialVersion.scenario, + } : { + name: card.name, + first_mes: card.first_mes, + description: card.description, + personality: card.personality, + mes_example: card.mes_example, + scenario: card.scenario, + }; + const element = document.createElement("a"); - const file = new Blob([JSON.stringify(card, null, 2)], { + const file = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json", }); element.href = URL.createObjectURL(file); - element.download = `${card.name.replace(/\s+/g, "_")}.json`; + element.download = `${card.name.replace(/[^a-zA-Z0-9\-_]/g, '_')}.json`; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; + const downloadChanges = async (card: Card) => { + try { + const response = await fetch(`/api/proxy?changes=true&cardId=${card.id}`); + if (!response.ok) { + throw new Error('Failed to fetch changes'); + } + + const changesData = await response.json(); + const element = document.createElement("a"); + const file = new Blob([JSON.stringify(changesData, null, 2)], { + type: "application/json", + }); + element.href = URL.createObjectURL(file); + element.download = `${card.name.replace(/[^a-zA-Z0-9\-_]/g, '_')}_changes.json`; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } catch (error) { + console.error("Error downloading changes:", error); + alert("Failed to download changes. The card may not have version history."); + } + }; + + const viewChanges = async (card: Card) => { + try { + const response = await fetch(`/api/proxy?changes=true&cardId=${card.id}`); + if (!response.ok) { + throw new Error('Failed to fetch changes'); + } + + const changesData = await response.json(); + setSelectedChanges(changesData); + setShowFullText(false); // Reset to diff view by default + setChangesDialogOpen(true); + } catch (error) { + console.error("Error fetching changes:", error); + alert("Failed to fetch changes. The card may not have version history."); + } + }; + const downloadPng = async (card: Card, cardId: string) => { if (!card.avatarUrl) return; @@ -115,18 +186,28 @@ export default function Home() { const arrayBuffer = await pngBlob.arrayBuffer(); - const cardData = JSON.stringify({ + // Use initial version for PNG embedding, or current version if no initial version available + const pngData = card.initialVersion ? { + name: card.initialVersion.name, + first_mes: card.initialVersion.first_mes, + description: card.initialVersion.description, + personality: card.initialVersion.personality, + mes_example: card.initialVersion.mes_example, + scenario: card.initialVersion.scenario, + } : { name: card.name, first_mes: card.first_mes, description: card.description, personality: card.personality, mes_example: card.mes_example, scenario: card.scenario, - }); + }; + + const cardData = JSON.stringify(pngData); const newImageData = Png.Generate(arrayBuffer, cardData); const newFileName = `${ - card.name.replace(/\s+/g, "_") || "character" + card.name.replace(/[^a-zA-Z0-9\-_]/g, '_') || "character" }.png`; const newFile = new File([newImageData], newFileName, { type: "image/png", @@ -191,9 +272,9 @@ export default function Home() {
-

Sucker v1.9

+

Sucker v2.0

- Handles the new prompt structure (again). See instructions below, you'll need it. + Now with multimessage support! Tracks changes to character descriptions and scenarios across multiple extractions.

@@ -277,6 +358,12 @@ export default function Home() { discarded. Reloading the page will remove any attached avatars. I'm not storing shit.

+

+ New: If you send multiple messages with the same character name, + Sucker will track changes to the description and scenario fields. Cards with + multiple versions will show a version badge and offer a "Download Changes" + button to get a detailed change history with timestamps. +

@@ -295,23 +382,37 @@ export default function Home() { - {card.name || "Unnamed Card"} +
+ {card.name || "Unnamed Card"} +
+ {card.hasVersions && ( + + v{card.versionCount} + + )} + {card.messageCount && card.messageCount > 1 && ( + + {card.messageCount} msgs + + )} +
+
- {card.description && ( + {(card.initialVersion?.description || card.description) && ( Description
-
{card.description}
+
{card.initialVersion?.description || card.description}
+ {card.hasVersions && ( + <> + + + + )} {!card.avatarUrl ? ( + + + + {selectedChanges && ( +
+
+
+ Total Versions: {selectedChanges.totalVersions} +
+
+ Current Version: {selectedChanges.currentVersion} +
+
+ Description Changes: {selectedChanges.summary.descriptionChanges} +
+
+ Scenario Changes: {selectedChanges.summary.scenarioChanges} +
+
+ + + +
+

Version History

+ {selectedChanges.versions.map((version: any, index: number) => ( +
+
+

+ Version {version.version} ({version.changeType}) +

+
+ {new Date(version.timestamp).toLocaleString()} + {version.messageCount && ` • Message ${version.messageCount}`} +
+
+ + {version.changes.description && ( +
+
Description Change:
+ {version.changeType === 'initial' ? ( +
+ Initial Content: {version.changes.description.new} +
+ ) : ( +
+ {version.addedText?.description && ( +
+
+
+ Added: {version.addedText.description} +
+ +
+
+ )} + {version.removedText?.description && ( +
+ Removed: {version.removedText.description} +
+ )} + {showFullText && ( +
+
+ Full Old: {version.changes.description.old} +
+
+ Full New: {version.changes.description.new} +
+
+ )} +
+ )} +
+ )} + + {version.changes.scenario && ( +
+
Scenario Change:
+ {version.changeType === 'initial' ? ( +
+ Initial Content: {version.changes.scenario.new} +
+ ) : ( +
+ {version.addedText?.scenario && ( +
+
+
+ Added: {version.addedText.scenario} +
+ +
+
+ )} + {version.removedText?.scenario && ( +
+ Removed: {version.removedText.scenario} +
+ )} + {showFullText && ( +
+
+ Full Old: {version.changes.scenario.old} +
+
+ Full New: {version.changes.scenario.new} +
+
+ )} +
+ )} +
+ )} +
+ ))} +
+
+ )} + + ); }