", fn.displayName);
- }
- {
- if (typeof fn === "function") {
- componentFrameCache.set(fn, _frame);
- }
- }
- return _frame;
- }
- } while (s >= 1 && c >= 0);
- }
- break;
- }
- }
- }
- } finally {
- reentry = false;
- {
- ReactCurrentDispatcher$1.current = previousDispatcher;
- reenableLogs();
- }
- Error.prepareStackTrace = previousPrepareStackTrace;
- }
- var name = fn ? fn.displayName || fn.name : "";
- var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
- {
- if (typeof fn === "function") {
- componentFrameCache.set(fn, syntheticFrame);
- }
- }
- return syntheticFrame;
- }
- function describeFunctionComponentFrame(fn, source, ownerFn) {
- {
- return describeNativeComponentFrame(fn, false);
- }
- }
- function shouldConstruct(Component2) {
- var prototype = Component2.prototype;
- return !!(prototype && prototype.isReactComponent);
- }
- function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
- if (type == null) {
- return "";
- }
- if (typeof type === "function") {
- {
- return describeNativeComponentFrame(type, shouldConstruct(type));
- }
- }
- if (typeof type === "string") {
- return describeBuiltInComponentFrame(type);
- }
- switch (type) {
- case REACT_SUSPENSE_TYPE:
- return describeBuiltInComponentFrame("Suspense");
- case REACT_SUSPENSE_LIST_TYPE:
- return describeBuiltInComponentFrame("SuspenseList");
- }
- if (typeof type === "object") {
- switch (type.$$typeof) {
- case REACT_FORWARD_REF_TYPE:
- return describeFunctionComponentFrame(type.render);
- case REACT_MEMO_TYPE:
- return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
- case REACT_LAZY_TYPE: {
- var lazyComponent = type;
- var payload = lazyComponent._payload;
- var init = lazyComponent._init;
- try {
- return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
- } catch (x) {
- }
- }
- }
- }
- return "";
- }
- var loggedTypeFailures = {};
- var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
- function setCurrentlyValidatingElement(element) {
- {
- if (element) {
- var owner = element._owner;
- var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
- ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
- } else {
- ReactDebugCurrentFrame$1.setExtraStackFrame(null);
- }
- }
- }
- function checkPropTypes(typeSpecs, values, location, componentName, element) {
- {
- var has = Function.call.bind(hasOwnProperty);
- for (var typeSpecName in typeSpecs) {
- if (has(typeSpecs, typeSpecName)) {
- var error$1 = void 0;
- try {
- if (typeof typeSpecs[typeSpecName] !== "function") {
- var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
- err.name = "Invariant Violation";
- throw err;
- }
- error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
- } catch (ex) {
- error$1 = ex;
- }
- if (error$1 && !(error$1 instanceof Error)) {
- setCurrentlyValidatingElement(element);
- error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
- setCurrentlyValidatingElement(null);
- }
- if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
- loggedTypeFailures[error$1.message] = true;
- setCurrentlyValidatingElement(element);
- error("Failed %s type: %s", location, error$1.message);
- setCurrentlyValidatingElement(null);
- }
- }
- }
- }
- }
- function setCurrentlyValidatingElement$1(element) {
- {
- if (element) {
- var owner = element._owner;
- var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
- setExtraStackFrame(stack);
- } else {
- setExtraStackFrame(null);
- }
- }
- }
- var propTypesMisspellWarningShown;
- {
- propTypesMisspellWarningShown = false;
- }
- function getDeclarationErrorAddendum() {
- if (ReactCurrentOwner.current) {
- var name = getComponentNameFromType(ReactCurrentOwner.current.type);
- if (name) {
- return "\n\nCheck the render method of `" + name + "`.";
- }
- }
- return "";
- }
- function getSourceInfoErrorAddendum(source) {
- if (source !== void 0) {
- var fileName = source.fileName.replace(/^.*[\\\/]/, "");
- var lineNumber = source.lineNumber;
- return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
- }
- return "";
- }
- function getSourceInfoErrorAddendumForProps(elementProps) {
- if (elementProps !== null && elementProps !== void 0) {
- return getSourceInfoErrorAddendum(elementProps.__source);
- }
- return "";
- }
- var ownerHasKeyUseWarning = {};
- function getCurrentComponentErrorInfo(parentType) {
- var info = getDeclarationErrorAddendum();
- if (!info) {
- var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
- if (parentName) {
- info = "\n\nCheck the top-level render call using <" + parentName + ">.";
- }
- }
- return info;
- }
- function validateExplicitKey(element, parentType) {
- if (!element._store || element._store.validated || element.key != null) {
- return;
- }
- element._store.validated = true;
- var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
- if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
- return;
- }
- ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
- var childOwner = "";
- if (element && element._owner && element._owner !== ReactCurrentOwner.current) {
- childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
- }
- {
- setCurrentlyValidatingElement$1(element);
- error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
- setCurrentlyValidatingElement$1(null);
- }
- }
- function validateChildKeys(node, parentType) {
- if (typeof node !== "object") {
- return;
- }
- if (isArray(node)) {
- for (var i = 0; i < node.length; i++) {
- var child = node[i];
- if (isValidElement(child)) {
- validateExplicitKey(child, parentType);
- }
- }
- } else if (isValidElement(node)) {
- if (node._store) {
- node._store.validated = true;
- }
- } else if (node) {
- var iteratorFn = getIteratorFn(node);
- if (typeof iteratorFn === "function") {
- if (iteratorFn !== node.entries) {
- var iterator = iteratorFn.call(node);
- var step;
- while (!(step = iterator.next()).done) {
- if (isValidElement(step.value)) {
- validateExplicitKey(step.value, parentType);
- }
- }
- }
- }
- }
- }
- function validatePropTypes(element) {
- {
- var type = element.type;
- if (type === null || type === void 0 || typeof type === "string") {
- return;
- }
- var propTypes;
- if (typeof type === "function") {
- propTypes = type.propTypes;
- } else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
- // Inner props are checked in the reconciler.
- type.$$typeof === REACT_MEMO_TYPE)) {
- propTypes = type.propTypes;
- } else {
- return;
- }
- if (propTypes) {
- var name = getComponentNameFromType(type);
- checkPropTypes(propTypes, element.props, "prop", name, element);
- } else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
- propTypesMisspellWarningShown = true;
- var _name = getComponentNameFromType(type);
- error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
- }
- if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
- error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
- }
- }
- }
- function validateFragmentProps(fragment) {
- {
- var keys = Object.keys(fragment.props);
- for (var i = 0; i < keys.length; i++) {
- var key = keys[i];
- if (key !== "children" && key !== "key") {
- setCurrentlyValidatingElement$1(fragment);
- error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
- setCurrentlyValidatingElement$1(null);
- break;
- }
- }
- if (fragment.ref !== null) {
- setCurrentlyValidatingElement$1(fragment);
- error("Invalid attribute `ref` supplied to `React.Fragment`.");
- setCurrentlyValidatingElement$1(null);
- }
- }
- }
- function createElementWithValidation(type, props, children) {
- var validType = isValidElementType(type);
- if (!validType) {
- var info = "";
- if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
- info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
- }
- var sourceInfo = getSourceInfoErrorAddendumForProps(props);
- if (sourceInfo) {
- info += sourceInfo;
- } else {
- info += getDeclarationErrorAddendum();
- }
- var typeString;
- if (type === null) {
- typeString = "null";
- } else if (isArray(type)) {
- typeString = "array";
- } else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
- typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
- info = " Did you accidentally export a JSX literal instead of a component?";
- } else {
- typeString = typeof type;
- }
- {
- error("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
- }
- }
- var element = createElement.apply(this, arguments);
- if (element == null) {
- return element;
- }
- if (validType) {
- for (var i = 2; i < arguments.length; i++) {
- validateChildKeys(arguments[i], type);
- }
- }
- if (type === REACT_FRAGMENT_TYPE) {
- validateFragmentProps(element);
- } else {
- validatePropTypes(element);
- }
- return element;
- }
- var didWarnAboutDeprecatedCreateFactory = false;
- function createFactoryWithValidation(type) {
- var validatedFactory = createElementWithValidation.bind(null, type);
- validatedFactory.type = type;
- {
- if (!didWarnAboutDeprecatedCreateFactory) {
- didWarnAboutDeprecatedCreateFactory = true;
- warn("React.createFactory() is deprecated and will be removed in a future major release. Consider using JSX or use React.createElement() directly instead.");
- }
- Object.defineProperty(validatedFactory, "type", {
- enumerable: false,
- get: function() {
- warn("Factory.type is deprecated. Access the class directly before passing it to createFactory.");
- Object.defineProperty(this, "type", {
- value: type
- });
- return type;
- }
- });
- }
- return validatedFactory;
- }
- function cloneElementWithValidation(element, props, children) {
- var newElement = cloneElement.apply(this, arguments);
- for (var i = 2; i < arguments.length; i++) {
- validateChildKeys(arguments[i], newElement.type);
- }
- validatePropTypes(newElement);
- return newElement;
- }
- function startTransition(scope, options) {
- var prevTransition = ReactCurrentBatchConfig.transition;
- ReactCurrentBatchConfig.transition = {};
- var currentTransition = ReactCurrentBatchConfig.transition;
- {
- ReactCurrentBatchConfig.transition._updatedFibers = /* @__PURE__ */ new Set();
- }
- try {
- scope();
- } finally {
- ReactCurrentBatchConfig.transition = prevTransition;
- {
- if (prevTransition === null && currentTransition._updatedFibers) {
- var updatedFibersCount = currentTransition._updatedFibers.size;
- if (updatedFibersCount > 10) {
- warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table.");
- }
- currentTransition._updatedFibers.clear();
- }
- }
- }
- }
- var didWarnAboutMessageChannel = false;
- var enqueueTaskImpl = null;
- function enqueueTask(task) {
- if (enqueueTaskImpl === null) {
- try {
- var requireString = ("require" + Math.random()).slice(0, 7);
- var nodeRequire = module && module[requireString];
- enqueueTaskImpl = nodeRequire.call(module, "timers").setImmediate;
- } catch (_err) {
- enqueueTaskImpl = function(callback) {
- {
- if (didWarnAboutMessageChannel === false) {
- didWarnAboutMessageChannel = true;
- if (typeof MessageChannel === "undefined") {
- error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning.");
- }
- }
- }
- var channel = new MessageChannel();
- channel.port1.onmessage = callback;
- channel.port2.postMessage(void 0);
- };
- }
- }
- return enqueueTaskImpl(task);
- }
- var actScopeDepth = 0;
- var didWarnNoAwaitAct = false;
- function act(callback) {
- {
- var prevActScopeDepth = actScopeDepth;
- actScopeDepth++;
- if (ReactCurrentActQueue.current === null) {
- ReactCurrentActQueue.current = [];
- }
- var prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy;
- var result;
- try {
- ReactCurrentActQueue.isBatchingLegacy = true;
- result = callback();
- if (!prevIsBatchingLegacy && ReactCurrentActQueue.didScheduleLegacyUpdate) {
- var queue = ReactCurrentActQueue.current;
- if (queue !== null) {
- ReactCurrentActQueue.didScheduleLegacyUpdate = false;
- flushActQueue(queue);
- }
- }
- } catch (error2) {
- popActScope(prevActScopeDepth);
- throw error2;
- } finally {
- ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy;
- }
- if (result !== null && typeof result === "object" && typeof result.then === "function") {
- var thenableResult = result;
- var wasAwaited = false;
- var thenable = {
- then: function(resolve, reject) {
- wasAwaited = true;
- thenableResult.then(function(returnValue2) {
- popActScope(prevActScopeDepth);
- if (actScopeDepth === 0) {
- recursivelyFlushAsyncActWork(returnValue2, resolve, reject);
- } else {
- resolve(returnValue2);
- }
- }, function(error2) {
- popActScope(prevActScopeDepth);
- reject(error2);
- });
- }
- };
- {
- if (!didWarnNoAwaitAct && typeof Promise !== "undefined") {
- Promise.resolve().then(function() {
- }).then(function() {
- if (!wasAwaited) {
- didWarnNoAwaitAct = true;
- error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);");
- }
- });
- }
- }
- return thenable;
- } else {
- var returnValue = result;
- popActScope(prevActScopeDepth);
- if (actScopeDepth === 0) {
- var _queue = ReactCurrentActQueue.current;
- if (_queue !== null) {
- flushActQueue(_queue);
- ReactCurrentActQueue.current = null;
- }
- var _thenable = {
- then: function(resolve, reject) {
- if (ReactCurrentActQueue.current === null) {
- ReactCurrentActQueue.current = [];
- recursivelyFlushAsyncActWork(returnValue, resolve, reject);
- } else {
- resolve(returnValue);
- }
- }
- };
- return _thenable;
- } else {
- var _thenable2 = {
- then: function(resolve, reject) {
- resolve(returnValue);
- }
- };
- return _thenable2;
- }
- }
- }
- }
- function popActScope(prevActScopeDepth) {
- {
- if (prevActScopeDepth !== actScopeDepth - 1) {
- error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
- }
- actScopeDepth = prevActScopeDepth;
- }
- }
- function recursivelyFlushAsyncActWork(returnValue, resolve, reject) {
- {
- var queue = ReactCurrentActQueue.current;
- if (queue !== null) {
- try {
- flushActQueue(queue);
- enqueueTask(function() {
- if (queue.length === 0) {
- ReactCurrentActQueue.current = null;
- resolve(returnValue);
- } else {
- recursivelyFlushAsyncActWork(returnValue, resolve, reject);
- }
- });
- } catch (error2) {
- reject(error2);
- }
- } else {
- resolve(returnValue);
- }
- }
- }
- var isFlushing = false;
- function flushActQueue(queue) {
- {
- if (!isFlushing) {
- isFlushing = true;
- var i = 0;
- try {
- for (; i < queue.length; i++) {
- var callback = queue[i];
- do {
- callback = callback(true);
- } while (callback !== null);
- }
- queue.length = 0;
- } catch (error2) {
- queue = queue.slice(i + 1);
- throw error2;
- } finally {
- isFlushing = false;
- }
- }
- }
- }
- var createElement$1 = createElementWithValidation;
- var cloneElement$1 = cloneElementWithValidation;
- var createFactory = createFactoryWithValidation;
- var Children = {
- map: mapChildren,
- forEach: forEachChildren,
- count: countChildren,
- toArray,
- only: onlyChild
- };
- exports.Children = Children;
- exports.Component = Component;
- exports.Fragment = REACT_FRAGMENT_TYPE;
- exports.Profiler = REACT_PROFILER_TYPE;
- exports.PureComponent = PureComponent;
- exports.StrictMode = REACT_STRICT_MODE_TYPE;
- exports.Suspense = REACT_SUSPENSE_TYPE;
- exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals;
- exports.act = act;
- exports.cloneElement = cloneElement$1;
- exports.createContext = createContext;
- exports.createElement = createElement$1;
- exports.createFactory = createFactory;
- exports.createRef = createRef;
- exports.forwardRef = forwardRef;
- exports.isValidElement = isValidElement;
- exports.lazy = lazy;
- exports.memo = memo;
- exports.startTransition = startTransition;
- exports.unstable_act = act;
- exports.useCallback = useCallback;
- exports.useContext = useContext;
- exports.useDebugValue = useDebugValue;
- exports.useDeferredValue = useDeferredValue;
- exports.useEffect = useEffect;
- exports.useId = useId;
- exports.useImperativeHandle = useImperativeHandle;
- exports.useInsertionEffect = useInsertionEffect;
- exports.useLayoutEffect = useLayoutEffect;
- exports.useMemo = useMemo;
- exports.useReducer = useReducer;
- exports.useRef = useRef;
- exports.useState = useState;
- exports.useSyncExternalStore = useSyncExternalStore;
- exports.useTransition = useTransition;
- exports.version = ReactVersion;
- if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined" && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop === "function") {
- __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error());
- }
- })();
- }
- }
-});
-
-// node_modules/react/index.js
-var require_react = __commonJS({
- "node_modules/react/index.js"(exports, module) {
- if (false) {
- module.exports = null;
- } else {
- module.exports = require_react_development();
- }
- }
-});
-
-export {
- require_react
-};
-/*! Bundled license information:
-
-react/cjs/react.development.js:
- (**
- * @license React
- * react.development.js
- *
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *)
-*/
-//# sourceMappingURL=chunk-CANBAPAS.js.map
diff --git a/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom.js b/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom.js
deleted file mode 100644
index e6ce44d..0000000
--- a/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import {
- require_react_dom
-} from "./chunk-TYILIMWK.js";
-import "./chunk-CANBAPAS.js";
-import "./chunk-5WRI5ZAA.js";
-export default require_react_dom();
-//# sourceMappingURL=react-dom.js.map
diff --git a/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom_client.js.map b/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom_client.js.map
deleted file mode 100644
index 544936a..0000000
--- a/frontend/node_modules/.vite/deps_temp_e32948ce/react-dom_client.js.map
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "version": 3,
- "sources": ["../../react-dom/client.js"],
- "sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
- "mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
- "names": []
-}
diff --git a/frontend/node_modules/.vite/deps_temp_e32948ce/react.js b/frontend/node_modules/.vite/deps_temp_e32948ce/react.js
deleted file mode 100644
index 7c2ec3f..0000000
--- a/frontend/node_modules/.vite/deps_temp_e32948ce/react.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import {
- require_react
-} from "./chunk-CANBAPAS.js";
-import "./chunk-5WRI5ZAA.js";
-export default require_react();
-//# sourceMappingURL=react.js.map
diff --git a/frontend/node_modules/.vite/deps_temp_e32948ce/zustand.js.map b/frontend/node_modules/.vite/deps_temp_e32948ce/zustand.js.map
deleted file mode 100644
index c7af1d2..0000000
--- a/frontend/node_modules/.vite/deps_temp_e32948ce/zustand.js.map
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "version": 3,
- "sources": ["../../use-sync-external-store/cjs/use-sync-external-store-shim.development.js", "../../use-sync-external-store/shim/index.js", "../../use-sync-external-store/cjs/use-sync-external-store-shim/with-selector.development.js", "../../use-sync-external-store/shim/with-selector.js", "../../zustand/esm/vanilla.mjs", "../../zustand/esm/index.mjs"],
- "sourcesContent": ["/**\n * @license React\n * use-sync-external-store-shim.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function is(x, y) {\n return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);\n }\n function useSyncExternalStore$2(subscribe, getSnapshot) {\n didWarnOld18Alpha ||\n void 0 === React.startTransition ||\n ((didWarnOld18Alpha = !0),\n console.error(\n \"You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release.\"\n ));\n var value = getSnapshot();\n if (!didWarnUncachedGetSnapshot) {\n var cachedValue = getSnapshot();\n objectIs(value, cachedValue) ||\n (console.error(\n \"The result of getSnapshot should be cached to avoid an infinite loop\"\n ),\n (didWarnUncachedGetSnapshot = !0));\n }\n cachedValue = useState({\n inst: { value: value, getSnapshot: getSnapshot }\n });\n var inst = cachedValue[0].inst,\n forceUpdate = cachedValue[1];\n useLayoutEffect(\n function () {\n inst.value = value;\n inst.getSnapshot = getSnapshot;\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n },\n [subscribe, value, getSnapshot]\n );\n useEffect(\n function () {\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n return subscribe(function () {\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n });\n },\n [subscribe]\n );\n useDebugValue(value);\n return value;\n }\n function checkIfSnapshotChanged(inst) {\n var latestGetSnapshot = inst.getSnapshot;\n inst = inst.value;\n try {\n var nextValue = latestGetSnapshot();\n return !objectIs(inst, nextValue);\n } catch (error) {\n return !0;\n }\n }\n function useSyncExternalStore$1(subscribe, getSnapshot) {\n return getSnapshot();\n }\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());\n var React = require(\"react\"),\n objectIs = \"function\" === typeof Object.is ? Object.is : is,\n useState = React.useState,\n useEffect = React.useEffect,\n useLayoutEffect = React.useLayoutEffect,\n useDebugValue = React.useDebugValue,\n didWarnOld18Alpha = !1,\n didWarnUncachedGetSnapshot = !1,\n shim =\n \"undefined\" === typeof window ||\n \"undefined\" === typeof window.document ||\n \"undefined\" === typeof window.document.createElement\n ? useSyncExternalStore$1\n : useSyncExternalStore$2;\n exports.useSyncExternalStore =\n void 0 !== React.useSyncExternalStore ? React.useSyncExternalStore : shim;\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());\n })();\n", "'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('../cjs/use-sync-external-store-shim.production.js');\n} else {\n module.exports = require('../cjs/use-sync-external-store-shim.development.js');\n}\n", "/**\n * @license React\n * use-sync-external-store-shim/with-selector.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function is(x, y) {\n return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);\n }\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());\n var React = require(\"react\"),\n shim = require(\"use-sync-external-store/shim\"),\n objectIs = \"function\" === typeof Object.is ? Object.is : is,\n useSyncExternalStore = shim.useSyncExternalStore,\n useRef = React.useRef,\n useEffect = React.useEffect,\n useMemo = React.useMemo,\n useDebugValue = React.useDebugValue;\n exports.useSyncExternalStoreWithSelector = function (\n subscribe,\n getSnapshot,\n getServerSnapshot,\n selector,\n isEqual\n ) {\n var instRef = useRef(null);\n if (null === instRef.current) {\n var inst = { hasValue: !1, value: null };\n instRef.current = inst;\n } else inst = instRef.current;\n instRef = useMemo(\n function () {\n function memoizedSelector(nextSnapshot) {\n if (!hasMemo) {\n hasMemo = !0;\n memoizedSnapshot = nextSnapshot;\n nextSnapshot = selector(nextSnapshot);\n if (void 0 !== isEqual && inst.hasValue) {\n var currentSelection = inst.value;\n if (isEqual(currentSelection, nextSnapshot))\n return (memoizedSelection = currentSelection);\n }\n return (memoizedSelection = nextSnapshot);\n }\n currentSelection = memoizedSelection;\n if (objectIs(memoizedSnapshot, nextSnapshot))\n return currentSelection;\n var nextSelection = selector(nextSnapshot);\n if (void 0 !== isEqual && isEqual(currentSelection, nextSelection))\n return (memoizedSnapshot = nextSnapshot), currentSelection;\n memoizedSnapshot = nextSnapshot;\n return (memoizedSelection = nextSelection);\n }\n var hasMemo = !1,\n memoizedSnapshot,\n memoizedSelection,\n maybeGetServerSnapshot =\n void 0 === getServerSnapshot ? null : getServerSnapshot;\n return [\n function () {\n return memoizedSelector(getSnapshot());\n },\n null === maybeGetServerSnapshot\n ? void 0\n : function () {\n return memoizedSelector(maybeGetServerSnapshot());\n }\n ];\n },\n [getSnapshot, getServerSnapshot, selector, isEqual]\n );\n var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);\n useEffect(\n function () {\n inst.hasValue = !0;\n inst.value = value;\n },\n [value]\n );\n useDebugValue(value);\n return value;\n };\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());\n })();\n", "'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('../cjs/use-sync-external-store-shim/with-selector.production.js');\n} else {\n module.exports = require('../cjs/use-sync-external-store-shim/with-selector.development.js');\n}\n", "const createStoreImpl = (createState) => {\n let state;\n const listeners = /* @__PURE__ */ new Set();\n const setState = (partial, replace) => {\n const nextState = typeof partial === \"function\" ? partial(state) : partial;\n if (!Object.is(nextState, state)) {\n const previousState = state;\n state = (replace != null ? replace : typeof nextState !== \"object\" || nextState === null) ? nextState : Object.assign({}, state, nextState);\n listeners.forEach((listener) => listener(state, previousState));\n }\n };\n const getState = () => state;\n const getInitialState = () => initialState;\n const subscribe = (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n const destroy = () => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") {\n console.warn(\n \"[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected.\"\n );\n }\n listeners.clear();\n };\n const api = { setState, getState, getInitialState, subscribe, destroy };\n const initialState = state = createState(setState, getState, api);\n return api;\n};\nconst createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;\nvar vanilla = (createState) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") {\n console.warn(\n \"[DEPRECATED] Default export is deprecated. Instead use import { createStore } from 'zustand/vanilla'.\"\n );\n }\n return createStore(createState);\n};\n\nexport { createStore, vanilla as default };\n", "import { createStore } from 'zustand/vanilla';\nexport * from 'zustand/vanilla';\nimport ReactExports from 'react';\nimport useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector.js';\n\nconst { useDebugValue } = ReactExports;\nconst { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;\nlet didWarnAboutEqualityFn = false;\nconst identity = (arg) => arg;\nfunction useStore(api, selector = identity, equalityFn) {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && equalityFn && !didWarnAboutEqualityFn) {\n console.warn(\n \"[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937\"\n );\n didWarnAboutEqualityFn = true;\n }\n const slice = useSyncExternalStoreWithSelector(\n api.subscribe,\n api.getState,\n api.getServerState || api.getInitialState,\n selector,\n equalityFn\n );\n useDebugValue(slice);\n return slice;\n}\nconst createImpl = (createState) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && typeof createState !== \"function\") {\n console.warn(\n \"[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.\"\n );\n }\n const api = typeof createState === \"function\" ? createStore(createState) : createState;\n const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn);\n Object.assign(useBoundStore, api);\n return useBoundStore;\n};\nconst create = (createState) => createState ? createImpl(createState) : createImpl;\nvar react = (createState) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") {\n console.warn(\n \"[DEPRECATED] Default export is deprecated. Instead use `import { create } from 'zustand'`.\"\n );\n }\n return create(createState);\n};\n\nexport { create, react as default, useStore };\n"],
- "mappings": "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",
- "names": ["useDebugValue", "useDebugValue", "ReactExports", "useSyncExternalStoreExports"]
-}
diff --git a/frontend/node_modules/.vite/deps_temp_e32948ce/zustand_middleware.js.map b/frontend/node_modules/.vite/deps_temp_e32948ce/zustand_middleware.js.map
deleted file mode 100644
index d1f9cc4..0000000
--- a/frontend/node_modules/.vite/deps_temp_e32948ce/zustand_middleware.js.map
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "version": 3,
- "sources": ["../../zustand/esm/middleware.mjs"],
- "sourcesContent": ["const reduxImpl = (reducer, initial) => (set, _get, api) => {\n api.dispatch = (action) => {\n set((state) => reducer(state, action), false, action);\n return action;\n };\n api.dispatchFromDevtools = true;\n return { dispatch: (...a) => api.dispatch(...a), ...initial };\n};\nconst redux = reduxImpl;\n\nconst trackedConnections = /* @__PURE__ */ new Map();\nconst getTrackedConnectionState = (name) => {\n const api = trackedConnections.get(name);\n if (!api) return {};\n return Object.fromEntries(\n Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])\n );\n};\nconst extractConnectionInformation = (store, extensionConnector, options) => {\n if (store === void 0) {\n return {\n type: \"untracked\",\n connection: extensionConnector.connect(options)\n };\n }\n const existingConnection = trackedConnections.get(options.name);\n if (existingConnection) {\n return { type: \"tracked\", store, ...existingConnection };\n }\n const newConnection = {\n connection: extensionConnector.connect(options),\n stores: {}\n };\n trackedConnections.set(options.name, newConnection);\n return { type: \"tracked\", store, ...newConnection };\n};\nconst devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {\n const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;\n let extensionConnector;\n try {\n extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") && window.__REDUX_DEVTOOLS_EXTENSION__;\n } catch (_e) {\n }\n if (!extensionConnector) {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && enabled) {\n console.warn(\n \"[zustand devtools middleware] Please install/enable Redux devtools extension\"\n );\n }\n return fn(set, get, api);\n }\n const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);\n let isRecording = true;\n api.setState = (state, replace, nameOrAction) => {\n const r = set(state, replace);\n if (!isRecording) return r;\n const action = nameOrAction === void 0 ? { type: anonymousActionType || \"anonymous\" } : typeof nameOrAction === \"string\" ? { type: nameOrAction } : nameOrAction;\n if (store === void 0) {\n connection == null ? void 0 : connection.send(action, get());\n return r;\n }\n connection == null ? void 0 : connection.send(\n {\n ...action,\n type: `${store}/${action.type}`\n },\n {\n ...getTrackedConnectionState(options.name),\n [store]: api.getState()\n }\n );\n return r;\n };\n const setStateFromDevtools = (...a) => {\n const originalIsRecording = isRecording;\n isRecording = false;\n set(...a);\n isRecording = originalIsRecording;\n };\n const initialState = fn(api.setState, get, api);\n if (connectionInformation.type === \"untracked\") {\n connection == null ? void 0 : connection.init(initialState);\n } else {\n connectionInformation.stores[connectionInformation.store] = api;\n connection == null ? void 0 : connection.init(\n Object.fromEntries(\n Object.entries(connectionInformation.stores).map(([key, store2]) => [\n key,\n key === connectionInformation.store ? initialState : store2.getState()\n ])\n )\n );\n }\n if (api.dispatchFromDevtools && typeof api.dispatch === \"function\") {\n let didWarnAboutReservedActionType = false;\n const originalDispatch = api.dispatch;\n api.dispatch = (...a) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && a[0].type === \"__setState\" && !didWarnAboutReservedActionType) {\n console.warn(\n '[zustand devtools middleware] \"__setState\" action type is reserved to set state from the devtools. Avoid using it.'\n );\n didWarnAboutReservedActionType = true;\n }\n originalDispatch(...a);\n };\n }\n connection.subscribe((message) => {\n var _a;\n switch (message.type) {\n case \"ACTION\":\n if (typeof message.payload !== \"string\") {\n console.error(\n \"[zustand devtools middleware] Unsupported action format\"\n );\n return;\n }\n return parseJsonThen(\n message.payload,\n (action) => {\n if (action.type === \"__setState\") {\n if (store === void 0) {\n setStateFromDevtools(action.state);\n return;\n }\n if (Object.keys(action.state).length !== 1) {\n console.error(\n `\n [zustand devtools middleware] Unsupported __setState action format. \n When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),\n and value of this only key should be a state object. Example: { \"type\": \"__setState\", \"state\": { \"abc123Store\": { \"foo\": \"bar\" } } }\n `\n );\n }\n const stateFromDevtools = action.state[store];\n if (stateFromDevtools === void 0 || stateFromDevtools === null) {\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {\n setStateFromDevtools(stateFromDevtools);\n }\n return;\n }\n if (!api.dispatchFromDevtools) return;\n if (typeof api.dispatch !== \"function\") return;\n api.dispatch(action);\n }\n );\n case \"DISPATCH\":\n switch (message.payload.type) {\n case \"RESET\":\n setStateFromDevtools(initialState);\n if (store === void 0) {\n return connection == null ? void 0 : connection.init(api.getState());\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"COMMIT\":\n if (store === void 0) {\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"ROLLBACK\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n setStateFromDevtools(state[store]);\n connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n });\n case \"JUMP_TO_STATE\":\n case \"JUMP_TO_ACTION\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {\n setStateFromDevtools(state[store]);\n }\n });\n case \"IMPORT_STATE\": {\n const { nextLiftedState } = message.payload;\n const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;\n if (!lastComputedState) return;\n if (store === void 0) {\n setStateFromDevtools(lastComputedState);\n } else {\n setStateFromDevtools(lastComputedState[store]);\n }\n connection == null ? void 0 : connection.send(\n null,\n // FIXME no-any\n nextLiftedState\n );\n return;\n }\n case \"PAUSE_RECORDING\":\n return isRecording = !isRecording;\n }\n return;\n }\n });\n return initialState;\n};\nconst devtools = devtoolsImpl;\nconst parseJsonThen = (stringified, f) => {\n let parsed;\n try {\n parsed = JSON.parse(stringified);\n } catch (e) {\n console.error(\n \"[zustand devtools middleware] Could not parse the received json\",\n e\n );\n }\n if (parsed !== void 0) f(parsed);\n};\n\nconst subscribeWithSelectorImpl = (fn) => (set, get, api) => {\n const origSubscribe = api.subscribe;\n api.subscribe = (selector, optListener, options) => {\n let listener = selector;\n if (optListener) {\n const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;\n let currentSlice = selector(api.getState());\n listener = (state) => {\n const nextSlice = selector(state);\n if (!equalityFn(currentSlice, nextSlice)) {\n const previousSlice = currentSlice;\n optListener(currentSlice = nextSlice, previousSlice);\n }\n };\n if (options == null ? void 0 : options.fireImmediately) {\n optListener(currentSlice, currentSlice);\n }\n }\n return origSubscribe(listener);\n };\n const initialState = fn(set, get, api);\n return initialState;\n};\nconst subscribeWithSelector = subscribeWithSelectorImpl;\n\nconst combine = (initialState, create) => (...a) => Object.assign({}, initialState, create(...a));\n\nfunction createJSONStorage(getStorage, options) {\n let storage;\n try {\n storage = getStorage();\n } catch (_e) {\n return;\n }\n const persistStorage = {\n getItem: (name) => {\n var _a;\n const parse = (str2) => {\n if (str2 === null) {\n return null;\n }\n return JSON.parse(str2, options == null ? void 0 : options.reviver);\n };\n const str = (_a = storage.getItem(name)) != null ? _a : null;\n if (str instanceof Promise) {\n return str.then(parse);\n }\n return parse(str);\n },\n setItem: (name, newValue) => storage.setItem(\n name,\n JSON.stringify(newValue, options == null ? void 0 : options.replacer)\n ),\n removeItem: (name) => storage.removeItem(name)\n };\n return persistStorage;\n}\nconst toThenable = (fn) => (input) => {\n try {\n const result = fn(input);\n if (result instanceof Promise) {\n return result;\n }\n return {\n then(onFulfilled) {\n return toThenable(onFulfilled)(result);\n },\n catch(_onRejected) {\n return this;\n }\n };\n } catch (e) {\n return {\n then(_onFulfilled) {\n return this;\n },\n catch(onRejected) {\n return toThenable(onRejected)(e);\n }\n };\n }\n};\nconst oldImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n getStorage: () => localStorage,\n serialize: JSON.stringify,\n deserialize: JSON.parse,\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage;\n try {\n storage = options.getStorage();\n } catch (_e) {\n }\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const thenableSerialize = toThenable(options.serialize);\n const setItem = () => {\n const state = options.partialize({ ...get() });\n let errorInSync;\n const thenable = thenableSerialize({ state, version: options.version }).then(\n (serializedValue) => storage.setItem(options.name, serializedValue)\n ).catch((e) => {\n errorInSync = e;\n });\n if (errorInSync) {\n throw errorInSync;\n }\n return thenable;\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n void setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n void setItem();\n },\n get,\n api\n );\n let stateFromStorage;\n const hydrate = () => {\n var _a;\n if (!storage) return;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => cb(get()));\n const postRehydrationCallback = ((_a = options.onRehydrateStorage) == null ? void 0 : _a.call(options, get())) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((storageValue) => {\n if (storageValue) {\n return options.deserialize(storageValue);\n }\n }).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n return options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n );\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return deserializedStorageValue.state;\n }\n }\n }).then((migratedState) => {\n var _a2;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n return setItem();\n }).then(() => {\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.getStorage) {\n storage = newOptions.getStorage();\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n hydrate();\n return stateFromStorage || configResult;\n};\nconst newImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n storage: createJSONStorage(() => localStorage),\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage = options.storage;\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const setItem = () => {\n const state = options.partialize({ ...get() });\n return storage.setItem(options.name, {\n state,\n version: options.version\n });\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n void setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n void setItem();\n },\n get,\n api\n );\n api.getInitialState = () => configResult;\n let stateFromStorage;\n const hydrate = () => {\n var _a, _b;\n if (!storage) return;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => {\n var _a2;\n return cb((_a2 = get()) != null ? _a2 : configResult);\n });\n const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n return [\n true,\n options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n )\n ];\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return [false, deserializedStorageValue.state];\n }\n }\n return [false, void 0];\n }).then((migrationResult) => {\n var _a2;\n const [migrated, migratedState] = migrationResult;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n if (migrated) {\n return setItem();\n }\n }).then(() => {\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n stateFromStorage = get();\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.storage) {\n storage = newOptions.storage;\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n if (!options.skipHydration) {\n hydrate();\n }\n return stateFromStorage || configResult;\n};\nconst persistImpl = (config, baseOptions) => {\n if (\"getStorage\" in baseOptions || \"serialize\" in baseOptions || \"deserialize\" in baseOptions) {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") {\n console.warn(\n \"[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead.\"\n );\n }\n return oldImpl(config, baseOptions);\n }\n return newImpl(config, baseOptions);\n};\nconst persist = persistImpl;\n\nexport { combine, createJSONStorage, devtools, persist, redux, subscribeWithSelector };\n"],
- "mappings": "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",
- "names": []
-}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 451f289..18cb075 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -5,6 +5,10 @@ import { ChatBox } from './components/Mid';
import SideBarLeft from './components/SideBarLeft';
import SideBarRight from './components/SideBarRight';
import useAppLayoutStore from './Store/AppLayoutSlice'; // ✅ 新增
+import useApiConfigStore from './Store/SideBarLeft/ApiConfigSlice'; // ✅ 引入 API 配置 Store
+import usePresetStore from './Store/SideBarLeft/PresetSlice'; // ✅ 引入预设 Store
+import useCharacterStore from './Store/SideBarLeft/CharacterSlice'; // ✅ 引入角色卡 Store
+import useWorldBookStore from './Store/SideBarLeft/WorldBookSlice'; // ✅ 引入世界书 Store
import './index.css';
function App() {
@@ -64,6 +68,125 @@ function App() {
};
}, []);
+ // ✅ 初始化时应用主题到 DOM(确保页面加载时就显示正确的主题)
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', colorTheme);
+ document.documentElement.setAttribute('data-color-theme', colorTheme);
+ }, [colorTheme]);
+
+ // ✅ 应用启动时自动加载必要的配置数据
+ useEffect(() => {
+ console.log('[App] 🚀 应用启动,开始加载默认配置...');
+ const startTime = Date.now();
+
+ // 获取各个 Store 的方法
+ const apiConfigStore = useApiConfigStore.getState();
+ const presetStore = usePresetStore.getState();
+ const characterStore = useCharacterStore.getState();
+ const worldBookStore = useWorldBookStore.getState();
+
+ // 并行加载所有必要的数据
+ Promise.allSettled([
+ // 1. 加载 API 配置文件列表
+ (async () => {
+ try {
+ await apiConfigStore.fetchProfiles();
+ console.log('[App] ✅ API 配置文件列表加载完成');
+
+ // ✅ 如果有持久化的配置 ID,优先使用;否则加载第一个
+ const persistedProfileId = apiConfigStore.currentProfileId;
+ const currentProfile = apiConfigStore.currentProfile;
+
+ if (persistedProfileId && !currentProfile) {
+ // 有持久化 ID 但没有详情,加载详情
+ console.log(`[App] 🔄 恢复上次选中的配置: ${persistedProfileId}`);
+ await apiConfigStore.fetchProfile(persistedProfileId);
+ console.log('[App] ✅ API 配置详情加载完成');
+ } else if (!currentProfile) {
+ // 没有持久化配置,加载第一个
+ const profiles = useApiConfigStore.getState().profiles;
+ if (profiles.length > 0) {
+ const firstProfile = profiles[0];
+ console.log(`[App] 📝 自动加载第一个配置文件: ${firstProfile.name}`);
+ await apiConfigStore.fetchProfile(firstProfile.id);
+ console.log('[App] ✅ API 配置详情加载完成');
+ } else {
+ console.warn('[App] ⚠️ 没有可用的 API 配置文件,请先到 API 配置页面创建');
+ }
+ } else {
+ console.log('[App] ✅ API 配置已从缓存恢复');
+ }
+ } catch (err) {
+ console.error('[App] ❌ API 配置加载失败:', err);
+ }
+ })(),
+
+ // 2. 加载预设列表
+ (async () => {
+ try {
+ await presetStore.fetchPresets();
+ console.log('[App] ✅ 预设列表加载完成');
+
+ // ✅ 如果有持久化的预设,优先使用
+ const persistedPreset = presetStore.selectedPreset;
+
+ if (persistedPreset) {
+ console.log(`[App] 🔄 恢复上次选中的预设: ${persistedPreset}`);
+ // 重新加载预设详情以获取最新配置
+ await presetStore.setSelectedPreset(persistedPreset);
+ console.log('[App] ✅ 预设详情加载完成');
+ } else {
+ // 没有持久化预设,选择第一个
+ const presets = usePresetStore.getState().presets;
+ if (presets.length > 0) {
+ const firstPreset = presets[0];
+ console.log(`[App] 📝 自动选择第一个预设: ${firstPreset.name}`);
+ presetStore.selectPreset(firstPreset.name);
+ } else {
+ console.warn('[App] ⚠️ 没有可用的预设');
+ }
+ }
+ } catch (err) {
+ console.error('[App] ❌ 预设列表加载失败:', err);
+ }
+ })(),
+
+ // 3. 加载角色卡列表
+ (async () => {
+ try {
+ await characterStore.fetchCharacters();
+ console.log('[App] ✅ 角色卡列表加载完成');
+ } catch (err) {
+ console.error('[App] ❌ 角色卡列表加载失败:', err);
+ }
+ })(),
+
+ // 4. 加载世界书列表
+ (async () => {
+ try {
+ await worldBookStore.fetchWorldBooks();
+ console.log('[App] ✅ 世界书列表加载完成');
+ } catch (err) {
+ console.error('[App] ❌ 世界书列表加载失败:', err);
+ }
+ })()
+ ]).then((results) => {
+ const endTime = Date.now();
+ const duration = ((endTime - startTime) / 1000).toFixed(2);
+
+ // 统计加载结果
+ const successCount = results.filter(r => r.status === 'fulfilled').length;
+ const failCount = results.filter(r => r.status === 'rejected').length;
+
+ console.log(`[App] 🎉 配置加载完成 (${duration}s)`);
+ console.log(`[App] 📊 成功: ${successCount}, 失败: ${failCount}`);
+
+ if (failCount > 0) {
+ console.warn('[App] ⚠️ 部分配置加载失败,但应用仍可正常使用');
+ }
+ });
+ }, []); // 仅在应用启动时执行一次
+
return (
{/* ✅ TopBar 不再需要 props,直接从 Store 读取状态 */}
diff --git a/frontend/src/Store/Mid/ChatBoxSlice.jsx b/frontend/src/Store/Mid/ChatBoxSlice.jsx
index b5f7923..66c9a1d 100644
--- a/frontend/src/Store/Mid/ChatBoxSlice.jsx
+++ b/frontend/src/Store/Mid/ChatBoxSlice.jsx
@@ -3,6 +3,8 @@ import { create } from 'zustand';
import { subscribeWithSelector, persist } from 'zustand/middleware';
import useApiConfigStore from '../SideBarLeft/ApiConfigSlice';
import usePresetStore from '../SideBarLeft/PresetSlice';
+import useCharacterStore from '../SideBarLeft/CharacterSlice'; // 引入角色卡 Store
+import useWorldBookStore from '../SideBarLeft/WorldBookSlice'; // 引入世界书 Store
const useChatBoxStore = create(
subscribeWithSelector(
@@ -39,8 +41,8 @@ const useChatBoxStore = create(
dynamicTable: false, // 动态表格
streamOutput: false, // 流式输出
imageWorkflow: false, // 生图工作流
- htmlRender: false, // HTML渲染
- markdownRender: true, // Markdown渲染(默认开启)
+ renderMode: 'markdown', // 渲染模式: 'none' | 'html' | 'markdown'
+ autoDiceRoll: false, // 自动掷骰子替换(默认关闭)
},
// 设置消息列表
@@ -80,11 +82,32 @@ const useChatBoxStore = create(
dynamicTable: false,
streamOutput: false,
imageWorkflow: false,
- htmlRender: false,
- markdownRender: true
+ renderMode: 'markdown',
+ autoDiceRoll: false
}
}),
+ // 切换渲染模式 (none -> html -> markdown -> none)
+ cycleRenderMode: () => set((state) => {
+ const modes = ['none', 'html', 'markdown'];
+ const currentIndex = modes.indexOf(state.options.renderMode);
+ const nextIndex = (currentIndex + 1) % modes.length;
+ return {
+ options: {
+ ...state.options,
+ renderMode: modes[nextIndex]
+ }
+ };
+ }),
+
+ // 设置渲染模式
+ setRenderMode: (mode) => set((state) => ({
+ options: {
+ ...state.options,
+ renderMode: mode
+ }
+ })),
+
// 同时设置角色和聊天
setChatBoxRoleAndChat: (role, chat) => {
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
@@ -108,6 +131,17 @@ const useChatBoxStore = create(
sendMessage: async (content) => {
const { messages, userName, characterName, currentRole, currentChat, options, wsConnection } = get();
+ // ✅ 如果启用了自动掷骰子,处理内容
+ let processedContent = content;
+ if (options.autoDiceRoll) {
+ const result = get().processDiceRoll(content);
+ processedContent = result.content;
+
+ if (result.hasDiceCommand) {
+ console.log('[DiceRoll] 自动替换掷骰指令:', { original: content, processed: processedContent });
+ }
+ }
+
// ✅ 如果没有 currentChat,先创建聊天文件
let actualChat = currentChat;
if (!currentChat && currentRole) {
@@ -147,6 +181,14 @@ const useChatBoxStore = create(
// 获取预设配置
const presetStore = usePresetStore.getState();
+ // ✅ 获取角色卡数据
+ const characterStore = useCharacterStore.getState();
+ const selectedCharacter = characterStore.selectedCharacter;
+
+ // ✅ 获取世界书数据
+ const worldBookStore = useWorldBookStore.getState();
+ const globalWorldBooks = worldBookStore.globalWorldBooks;
+
// 关闭之前的WebSocket连接
if (wsConnection) {
wsConnection.close();
@@ -161,7 +203,7 @@ const useChatBoxStore = create(
messages: [...messages, {
id: Date.now(),
floor: nextFloor,
- mes: content,
+ mes: processedContent, // 使用处理后的内容
is_user: true,
name: userName || 'User',
sendDate: new Date().toISOString()
@@ -170,8 +212,10 @@ const useChatBoxStore = create(
try {
// 统一使用WebSocket处理流式和非流式输出
- const backendUrl = import.meta.env.VITE_API_URL || 'http://localhost:23337';
- const wsUrl = `${backendUrl.replace(/^http/, 'ws')}/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(actualChat)}/ws`;
+ // WebSocket 直接连接到后端,使用浏览器的 host
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsHost = window.location.host.replace('23338', '23337'); // 前端 23338 -> 后端 23337
+ const wsUrl = `${wsProtocol}//${wsHost}/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(actualChat)}/ws`;
const ws = new WebSocket(wsUrl);
console.log('[WebSocket] 正在建立连接...', { url: wsUrl });
@@ -210,23 +254,34 @@ const useChatBoxStore = create(
ws.onopen = () => {
clearTimeout(connectionTimeout); // 清除超时定时器
- console.log('[WebSocket] 连接已建立', { readyState: ws.readyState });
+ console.log('\n' + '='.repeat(80));
+ console.log('[WebSocket] 📡 连接已建立');
+ console.log(' - URL:', wsUrl);
+ console.log(' - Ready State:', ws.readyState);
+ console.log('='.repeat(80) + '\n');
};
ws.onclose = (event) => {
- console.log('[WebSocket] 连接已关闭', {
- code: event.code,
- reason: event.reason,
- wasClean: event.wasClean
- });
+ console.log('\n' + '='.repeat(80));
+ console.log('[WebSocket] 🔌 连接已关闭');
+ console.log(' - Code:', event.code);
+ console.log(' - Reason:', event.reason);
+ console.log(' - Was Clean:', event.wasClean);
+ console.log('='.repeat(80) + '\n');
};
// 处理WebSocket消息
+ let chunkCount = 0;
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
- console.log('[WebSocket] 收到消息', { type: data.type, content: data.content });
-
+
if (data.type === 'chunk') {
+ chunkCount++;
+ // 每10个chunk记录一次
+ if (chunkCount % 10 === 0) {
+ console.log(`[WebSocket] 📊 已接收 ${chunkCount} 个 chunks`);
+ }
+
// 处理流式数据块
assistantMessage += data.content;
set((state) => ({
@@ -234,12 +289,68 @@ const useChatBoxStore = create(
msg.id === newMessageId ? { ...msg, mes: assistantMessage } : msg
)
}));
+ } else if (data.type === 'worldbook_active') {
+ console.log('[WebSocket] 📚 收到世界书激活信息:', data.entries.length, '个条目');
+ // ✅ 更新世界书激活显示
+ import('../../Store/SideBarRight/WorldBookActiveSlice').then(module => {
+ module.default.getState().setActiveEntries(data.entries);
+ });
+ } else if (data.type === 'tasks_created') {
+ console.log('[WebSocket] 📋 收到任务ID信息:', data.tasks);
+ // ✅ 创建了新任务
+ import('../../Store/SideBarRight/TasksSlice').then(module => {
+ const tasksStore = module.default;
+ const newTasks = [];
+
+ if (data.tasks.imageWorkflow) {
+ newTasks.push({
+ taskId: data.tasks.imageWorkflow,
+ taskType: 'image_workflow',
+ chatId: `${currentRole}/${actualChat}`
+ });
+ }
+
+ if (data.tasks.dynamicTable) {
+ newTasks.push({
+ taskId: data.tasks.dynamicTable,
+ taskType: 'dynamic_table',
+ chatId: `${currentRole}/${actualChat}`
+ });
+ }
+
+ if (newTasks.length > 0) {
+ tasksStore.getState().addTasks(newTasks);
+ }
+ });
+ } else if (data.type === 'task_status_update') {
+ console.log('[WebSocket] 🔄 收到任务状态更新:', data.tasks);
+ // ✅ 任务状态更新
+ import('../../Store/SideBarRight/TasksSlice').then(module => {
+ module.default.getState().setTasks(data.tasks);
+ });
+ } else if (data.type === 'task_cancelled') {
+ console.log('[WebSocket] ❌ 任务取消确认:', data.taskId);
+ // ✅ 任务取消确认
+ import('../../Store/SideBarRight/TasksSlice').then(module => {
+ module.default.getState().updateTaskStatus(data.taskId, 'cancelled');
+ });
+ } else if (data.type === 'interrupted') {
+ console.log('\n[WebSocket] 🛑 收到中断信号');
+ console.log(' - 已生成内容长度:', data.content?.length || 0);
+ // ✅ 处理中断:保存已生成的部分内容
+ isStreamComplete = true;
+ ws.close();
+ set({ wsConnection: null, isGenerating: false });
} else if (data.type === 'complete') {
+ console.log('\n[WebSocket] ✅ 收到完成信号');
+ console.log(' - 总 Chunks:', chunkCount);
+ console.log(' - 消息长度:', assistantMessage.length);
// 完成响应
isStreamComplete = true;
ws.close();
set({ wsConnection: null, isGenerating: false });
} else if (data.type === 'error') {
+ console.error('[WebSocket] ❌ 收到错误:', data.message);
// 错误处理
set({
error: data.message,
@@ -272,23 +383,74 @@ const useChatBoxStore = create(
// 发送请求到WebSocket(确保连接已建立)
const sendAfterConnect = () => {
if (ws.readyState === WebSocket.OPEN) {
- console.log('[WebSocket] 发送消息', { readyState: ws.readyState });
+ // ✅ 获取 API 配置
+ const apiConfigData = {
+ api_url: apiConfigStore.currentProfile?.apis?.mainLLM?.apiUrl || '',
+ api_key: apiConfigStore.currentProfile?.apis?.mainLLM?.apiKey ? '***' : '',
+ model: apiConfigStore.currentProfile?.apis?.mainLLM?.model || ''
+ };
+
+ console.log('\n' + '-'.repeat(80));
+ console.log('[WebSocket] 📤 发送消息:');
+ console.log(' - Floor:', nextFloor);
+ console.log(' - Role:', currentRole);
+ console.log(' - Chat:', actualChat);
+ console.log(' - Stream:', options.streamOutput);
+ console.log(' - Message Length:', processedContent.length);
+ console.log(' - API Config:', apiConfigData);
+ console.log(' - Current Profile:', apiConfigStore.currentProfile);
+ console.log('-'.repeat(80) + '\n');
+
ws.send(JSON.stringify({
floor: nextFloor,
- mes: content,
+ mes: processedContent, // 使用处理后的内容
is_user: true,
currentRole: currentRole,
currentChat: actualChat, // 使用 actualChat
options: options,
apiConfig: {
- api_url: apiConfigStore.allApis.find(api => api.category === 'text' && api.id === apiConfigStore.activeMap.text)?.api_url || '',
- api_key: apiConfigStore.allApis.find(api => api.category === 'text' && api.id === apiConfigStore.activeMap.text)?.api_key || ''
+ // 从 currentProfile 中获取 mainLLM 配置(不包含 apiKey)
+ api_url: apiConfigStore.currentProfile?.apis?.mainLLM?.apiUrl || '',
+ model: apiConfigStore.currentProfile?.apis?.mainLLM?.model || ''
+ },
+ // ✅ 传递 profileId,让后端从配置文件读取 API Key
+ currentProfile: {
+ id: apiConfigStore.currentProfile?.id || null
},
presetConfig: {
selectedPreset: presetStore.selectedPreset,
parameters: presetStore.parameters,
promptComponents: presetStore.promptComponents
},
+ // ✅ 角色卡数据
+ characterData: selectedCharacter ? {
+ id: selectedCharacter.id,
+ name: selectedCharacter.name,
+ description: selectedCharacter.description,
+ personality: selectedCharacter.personality,
+ scenario: selectedCharacter.scenario,
+ first_mes: selectedCharacter.first_mes,
+ mes_example: selectedCharacter.mes_example,
+ worldInfoId: selectedCharacter.worldInfoId || null, // 绑定的世界书ID
+ tags: selectedCharacter.tags || [],
+ categories: selectedCharacter.categories || []
+ } : null,
+ // ✅ 世界书数据
+ worldBookData: {
+ globalBooks: globalWorldBooks.map(wb => ({
+ id: wb.id,
+ name: wb.name,
+ description: wb.description
+ })),
+ characterBookId: selectedCharacter?.worldInfoId || null // 角色绑定的世界书ID
+ },
+ // ✅ 动态表格数据(如果启用)
+ dynamicTableData: options.dynamicTable ? {
+ headers: selectedCharacter?.tableHeaders || [],
+ currentValues: selectedCharacter?.tableDefaults || {}
+ } : null,
+ // ✅ 时间戳(用于冲突解决)
+ timestamp: Date.now(),
stream: options.streamOutput
}));
} else if (ws.readyState === WebSocket.CONNECTING) {
@@ -317,9 +479,24 @@ const useChatBoxStore = create(
// 终止生成
stopGeneration: () => set((state) => {
- if (state.wsConnection) {
- state.wsConnection.close();
+ if (state.wsConnection && state.wsConnection.readyState === WebSocket.OPEN) {
+ console.log('[ChatBoxStore] 🛑 发送终止信号...');
+
+ // ✅ 先发送取消任务信号给后端
+ state.wsConnection.send(JSON.stringify({
+ type: 'cancel_task',
+ taskId: 'current_llm_generation' // 标记为当前LLM生成任务
+ }));
+
+ // 等待一小段时间让后端处理
+ setTimeout(() => {
+ if (state.wsConnection) {
+ state.wsConnection.close();
+ console.log('[ChatBoxStore] 🔌 WebSocket 已关闭');
+ }
+ }, 100);
}
+
return {
isGenerating: false,
wsConnection: null
@@ -517,7 +694,37 @@ const useChatBoxStore = create(
partialize: (state) => ({
// 只持久化选项状态,不持久化聊天历史等
options: state.options
- })
+ }),
+ merge: (persistedState, currentState) => {
+ // 合并持久化状态和当前状态
+ const merged = {
+ ...currentState,
+ ...persistedState,
+ };
+
+ // 确保 options 中的所有字段都存在,使用默认值填充缺失的字段
+ if (persistedState?.options) {
+ merged.options = {
+ dynamicTable: persistedState.options.dynamicTable ?? false,
+ streamOutput: persistedState.options.streamOutput ?? false,
+ imageWorkflow: persistedState.options.imageWorkflow ?? false,
+ // 兼容旧版本:如果存在 htmlRender/markdownRender,转换为 renderMode
+ renderMode: (() => {
+ // 优先使用新的 renderMode
+ if (persistedState.options.renderMode) {
+ return persistedState.options.renderMode;
+ }
+ // 兼容旧版本的 htmlRender/markdownRender
+ if (persistedState.options.markdownRender) return 'markdown';
+ if (persistedState.options.htmlRender) return 'html';
+ return 'none';
+ })(),
+ autoDiceRoll: persistedState.options.autoDiceRoll ?? false,
+ };
+ }
+
+ return merged;
+ }
}
)
)
diff --git a/frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx b/frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx
index 18d8fe7..4613816 100644
--- a/frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx
+++ b/frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx
@@ -27,6 +27,7 @@ const useApiConfigStore = create(
set({ loading: true, error: null });
try {
const response = await fetch('/api/api-config/profiles');
+
if (!response.ok) {
throw new Error('Failed to fetch profiles');
}
@@ -47,7 +48,11 @@ const useApiConfigStore = create(
throw new Error('Failed to fetch profile');
}
const data = await response.json();
- set({ currentProfile: data, loading: false });
+ set({
+ currentProfile: data,
+ currentProfileId: profileId, // ✅ 保存当前配置 ID
+ loading: false
+ });
return data;
} catch (err) {
set({ error: err.message, loading: false });
@@ -193,8 +198,27 @@ const useApiConfigStore = create(
{
name: 'ApiConfigStore',
partialize: (state) => ({
- activeMap: state.activeMap
- })
+ activeMap: state.activeMap,
+ // ✅ 持久化当前选中的配置文件 ID
+ currentProfileId: state.currentProfile?.id || null
+ }),
+ // ✅ 恢复时自动加载对应的配置详情
+ onRehydrateStorage: () => (state, error) => {
+ if (error) {
+ console.error('[ApiConfigStore] 恢复状态失败:', error);
+ return;
+ }
+
+ if (state?.currentProfileId) {
+ console.log(`[ApiConfigStore] 🔄 恢复上次选中的配置: ${state.currentProfileId}`);
+ // 异步加载配置详情
+ setTimeout(() => {
+ useApiConfigStore.getState().fetchProfile(state.currentProfileId).catch(err => {
+ console.error('[ApiConfigStore] 加载配置详情失败:', err);
+ });
+ }, 0);
+ }
+ }
}
)
);
diff --git a/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx b/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx
index 82bc358..c903da4 100644
--- a/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx
+++ b/frontend/src/Store/SideBarLeft/CharacterCardUISlice.jsx
@@ -7,8 +7,9 @@ import { create } from 'zustand';
const useCharacterCardUIStore = create((set) => ({
// ==================== 状态 ====================
- // 筛选标签
- filterTag: '',
+ // 筛选标签数组 - 支持多标签交集筛选
+ // 格式: ['include:tag1', 'exclude:tag2', 'include:tag3']
+ filterTags: [],
// 是否处于编辑模式
isEditing: false,
@@ -25,11 +26,27 @@ const useCharacterCardUIStore = create((set) => ({
// ==================== Actions ====================
/**
- * 设置筛选标签
+ * 设置筛选标签(三次切换:无筛选 -> 包含 -> 排除 -> 无筛选)
* @param {string} tag - 标签名
*/
setFilterTag: (tag) => {
- set({ filterTag: tag, currentPage: 1 }); // 重置页码
+ set((state) => {
+ const currentFilter = state.filterTag;
+
+ // 如果点击的是同一个标签,循环切换状态
+ if (currentFilter && (currentFilter === tag || currentFilter === `include:${tag}` || currentFilter === `exclude:${tag}`)) {
+ if (currentFilter === tag || currentFilter === `include:${tag}`) {
+ // 从包含切换到排除
+ return { filterTag: `exclude:${tag}`, currentPage: 1 };
+ } else {
+ // 从排除切换到无筛选
+ return { filterTag: '', currentPage: 1 };
+ }
+ } else {
+ // 点击新标签,设置为包含模式
+ return { filterTag: `include:${tag}`, currentPage: 1 };
+ }
+ });
},
/**
@@ -54,7 +71,8 @@ const useCharacterCardUIStore = create((set) => ({
first_mes: character.first_mes || '',
mes_example: character.mes_example || '',
categories: character.categories || [],
- tags: character.tags || []
+ tags: character.tags || [],
+ worldInfoId: character.worldInfoId || null
}
});
},
diff --git a/frontend/src/Store/SideBarLeft/PresetSlice.jsx b/frontend/src/Store/SideBarLeft/PresetSlice.jsx
index 6bc2b6e..a512252 100644
--- a/frontend/src/Store/SideBarLeft/PresetSlice.jsx
+++ b/frontend/src/Store/SideBarLeft/PresetSlice.jsx
@@ -1,6 +1,9 @@
import { create } from 'zustand';
+import { persist } from 'zustand/middleware'; // ✅ 添加持久化支持
-const usePresetStore = create((set, get) => ({
+const usePresetStore = create(
+ persist(
+ (set, get) => ({
// 预设选择
selectedPreset: '',
@@ -405,6 +408,47 @@ const usePresetStore = create((set, get) => ({
const { presets, pageSize } = get();
return Math.ceil(presets.length / pageSize);
}
-}));
+ }), // ✅ 闭合 (set, get) => ({...})
+ {
+ name: 'PresetStore', // localStorage 中的键名
+ partialize: (state) => ({
+ // ✅ 持久化选中的预设名称
+ selectedPreset: state.selectedPreset,
+ // ✅ 持久化参数设置
+ parameters: state.parameters,
+ // ✅ 持久化提示词组件配置
+ promptComponents: state.promptComponents,
+ // ✅ 持久化折叠状态
+ isParametersExpanded: state.isParametersExpanded
+ }),
+ // ✅ 恢复时的回调
+ onRehydrateStorage: () => {
+ return (state, error) => {
+ if (error) {
+ console.error('[PresetStore] 恢复状态失败:', error);
+ return;
+ }
+
+ if (state?.selectedPreset) {
+ console.log(`[PresetStore] 🔄 恢复上次选中的预设: ${state.selectedPreset}`);
+ // 异步加载预设详情
+ setTimeout(() => {
+ usePresetStore.getState().fetchPresets().then(() => {
+ // 加载完列表后,重新选择之前选中的预设以加载其详细配置
+ if (state.selectedPreset) {
+ usePresetStore.getState().setSelectedPreset(state.selectedPreset).catch(err => {
+ console.error('[PresetStore] 加载预设详情失败:', err);
+ });
+ }
+ }).catch(err => {
+ console.error('[PresetStore] 加载预设列表失败:', err);
+ });
+ }, 0);
+ }
+ };
+ }
+ }
+ ) // ✅ 闭合 persist(
+);
export default usePresetStore;
diff --git a/frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx b/frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx
index 3a9393d..d4b83e8 100644
--- a/frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx
+++ b/frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx
@@ -11,7 +11,8 @@ const useSideBarLeftStore = create(
{ id: 'character', label: '角色', title: '管理AI角色卡' },
{ id: 'api', label: 'API', title: '配置LLM和生图、向量化API连接' },
{ id: 'presets', label: '预设', title: '管理对话预设和系统提示词' },
- { id: 'worldbook', label: '世界', title: '管理世界观设定和背景知识' }
+ { id: 'worldbook', label: '世界', title: '管理世界观设定和背景知识' },
+ { id: 'tokenUsage', label: 'Token', title: '查看 Token 使用统计' }
],
setActiveTab: (tab) => set({ activeTab: tab })
diff --git a/frontend/src/Store/SideBarLeft/TokenUsageSlice.jsx b/frontend/src/Store/SideBarLeft/TokenUsageSlice.jsx
new file mode 100644
index 0000000..1a60123
--- /dev/null
+++ b/frontend/src/Store/SideBarLeft/TokenUsageSlice.jsx
@@ -0,0 +1,122 @@
+/**
+ * Token 使用统计 Store
+ */
+import { create } from 'zustand';
+
+const useTokenUsageStore = create((set, get) => ({
+ // 状态
+ months: [],
+ currentMonth: null,
+ stats: null,
+ roles: [],
+ chats: [],
+ selectedRole: null,
+ selectedChat: null,
+ loading: false,
+ error: null,
+
+ // Actions
+ fetchMonths: async () => {
+ try {
+ set({ loading: true, error: null });
+ const response = await fetch('/api/token-usage/months');
+ if (!response.ok) throw new Error('Failed to fetch months');
+
+ const months = await response.json();
+ set({ months, loading: false });
+
+ // 默认选择最新的月份
+ if (months.length > 0) {
+ const latest = months[months.length - 1];
+ await get().fetchStats(latest.year, latest.month);
+ }
+ } catch (error) {
+ set({ error: error.message, loading: false });
+ }
+ },
+
+ fetchStats: async (year, month, role = null, chat = null) => {
+ try {
+ set({ loading: true, error: null, currentMonth: { year, month } });
+
+ let url = `/api/token-usage/stats/${year}/${month}`;
+ const params = new URLSearchParams();
+ if (role) params.append('role_name', role);
+ if (chat) params.append('chat_name', chat);
+
+ if (params.toString()) {
+ url += `?${params.toString()}`;
+ }
+
+ const response = await fetch(url);
+ if (!response.ok) throw new Error('Failed to fetch stats');
+
+ const stats = await response.json();
+ set({ stats, loading: false });
+ } catch (error) {
+ set({ error: error.message, loading: false });
+ }
+ },
+
+ fetchRoles: async (year, month) => {
+ try {
+ const response = await fetch(`/api/token-usage/roles/${year}/${month}`);
+ if (!response.ok) throw new Error('Failed to fetch roles');
+
+ const data = await response.json();
+ set({ roles: data.roles });
+ } catch (error) {
+ console.error('Failed to fetch roles:', error);
+ }
+ },
+
+ fetchChats: async (year, month, role = null) => {
+ try {
+ let url = `/api/token-usage/chats/${year}/${month}`;
+ if (role) {
+ url += `?role_name=${encodeURIComponent(role)}`;
+ }
+
+ const response = await fetch(url);
+ if (!response.ok) throw new Error('Failed to fetch chats');
+
+ const data = await response.json();
+ set({ chats: data.chats });
+ } catch (error) {
+ console.error('Failed to fetch chats:', error);
+ }
+ },
+
+ setSelectedRole: (role) => {
+ set({ selectedRole: role });
+ const { currentMonth } = get();
+ if (currentMonth) {
+ get().fetchStats(currentMonth.year, currentMonth.month, role, get().selectedChat);
+ }
+ },
+
+ setSelectedChat: (chat) => {
+ set({ selectedChat: chat });
+ const { currentMonth } = get();
+ if (currentMonth) {
+ get().fetchStats(currentMonth.year, currentMonth.month, get().selectedRole, chat);
+ }
+ },
+
+ setCurrentMonth: (year, month) => {
+ set({ currentMonth: { year, month } });
+ get().fetchStats(year, month, get().selectedRole, get().selectedChat);
+ get().fetchRoles(year, month);
+ get().fetchChats(year, month, get().selectedRole);
+ },
+
+ resetFilters: () => {
+ set({ selectedRole: null, selectedChat: null });
+ const { currentMonth } = get();
+ if (currentMonth) {
+ get().fetchStats(currentMonth.year, currentMonth.month);
+ }
+ },
+}));
+
+export default useTokenUsageStore;
diff --git a/frontend/src/Store/SideBarRight/SideBarRightSlice.jsx b/frontend/src/Store/SideBarRight/SideBarRightSlice.jsx
index 8b86e22..600a2fd 100644
--- a/frontend/src/Store/SideBarRight/SideBarRightSlice.jsx
+++ b/frontend/src/Store/SideBarRight/SideBarRightSlice.jsx
@@ -3,15 +3,16 @@ import { persist } from 'zustand/middleware';
const useSideBarRightStore = create(
persist(
- (set) => ({
+ (set, get) => ({
selectedTabs: ['dice', 'rag'],
allTabs: [
{ id: 'dice', label: '骰子', title: '掷骰子和随机数生成工具', component: null },
- { id: 'debug', label: '调试', title: '查看具体发送了哪些上下文', component: null },
{ id: 'macros', label: '宏', title: '主要为快捷输入', component: null },
{ id: 'table', label: '表格', title: '动态数据表格展示与编辑,可直接影响后端', component: null },
- { id: 'rag', label: 'RAG', title: '查看具体召回了哪些条目', component: null }
+ { id: 'rag', label: 'RAG', title: '查看具体召回了哪些条目', component: null },
+ { id: 'worldbook_active', label: '世界书', title: '查看当前对话中激活的世界书条目', component: null }, // ✅ 添加世界书激活标签
+ { id: 'tasks', label: '任务', title: '查看和管理并行任务(生图、表格维护等)', component: null } // ✅ 添加任务队列标签
],
handleTabClick: (tabId) => set((state) => {
@@ -32,11 +33,46 @@ const useSideBarRightStore = create(
allTabs: state.allTabs.map(tab =>
tab.id === tabId ? { ...tab, component } : tab
)
- }))
+ })),
+
+ // ✅ 验证并清理无效的选中标签
+ validateSelectedTabs: () => {
+ const state = get();
+ const validIds = state.allTabs.map(tab => tab.id);
+ const validSelectedTabs = state.selectedTabs.filter(id => validIds.includes(id));
+
+ if (validSelectedTabs.length !== state.selectedTabs.length) {
+ set({ selectedTabs: validSelectedTabs });
+ }
+ }
}),
{
name: 'sidebar-right-storage', // localStorage key
- partialize: (state) => ({ selectedTabs: state.selectedTabs }) // 只保存 selectedTabs
+ partialize: (state) => ({ selectedTabs: state.selectedTabs }), // 只保存 selectedTabs
+ // ✅ 迁移逻辑:清除已删除的标签
+ migrate: (persistedState, version) => {
+ if (persistedState.selectedTabs) {
+ // 移除已删除的 'debug' 标签
+ const cleanedTabs = persistedState.selectedTabs.filter(id => id !== 'debug');
+
+ // 如果清理后为空或只有一个,恢复到默认值
+ if (cleanedTabs.length === 0) {
+ persistedState.selectedTabs = ['dice', 'rag'];
+ } else if (cleanedTabs.length === 1 && cleanedTabs[0] === 'dice') {
+ // 如果只剩dice,添加rag作为第二个
+ persistedState.selectedTabs = ['dice', 'rag'];
+ } else {
+ persistedState.selectedTabs = cleanedTabs;
+ }
+ }
+ return persistedState;
+ },
+ // ✅ 加载后验证
+ onRehydrateStorage: () => (state) => {
+ if (state) {
+ state.validateSelectedTabs();
+ }
+ }
}
)
);
diff --git a/frontend/src/Store/SideBarRight/TasksSlice.jsx b/frontend/src/Store/SideBarRight/TasksSlice.jsx
new file mode 100644
index 0000000..d43ab5d
--- /dev/null
+++ b/frontend/src/Store/SideBarRight/TasksSlice.jsx
@@ -0,0 +1,88 @@
+import { create } from 'zustand';
+import useChatBoxStore from '../Mid/ChatBoxSlice';
+
+/**
+ * 任务队列 Store
+ * 管理并行任务(生图、动态表格维护等)的状态
+ */
+const useTasksStore = create((set, get) => ({
+ // ==================== 状态 ====================
+
+ // 任务列表
+ tasks: [],
+
+ // ==================== Actions ====================
+
+ /**
+ * 添加任务
+ * @param {Object} task - 任务对象 {taskId, taskType, chatId}
+ */
+ addTask: (task) => set((state) => ({
+ tasks: [...state.tasks, { ...task, status: 'pending' }]
+ })),
+
+ /**
+ * 批量添加任务
+ * @param {Array} newTasks - 新任务数组
+ */
+ addTasks: (newTasks) => set((state) => ({
+ tasks: [...state.tasks, ...newTasks.map(t => ({ ...t, status: 'pending' }))]
+ })),
+
+ /**
+ * 更新任务状态
+ * @param {string} taskId - 任务ID
+ * @param {string} status - 新状态
+ * @param {Object} metadata - 额外元数据
+ */
+ updateTaskStatus: (taskId, status, metadata = null) => set((state) => ({
+ tasks: state.tasks.map(task =>
+ task.taskId === taskId
+ ? { ...task, status, ...(metadata && { metadata }) }
+ : task
+ )
+ })),
+
+ /**
+ * 取消任务
+ * @param {string} taskId - 任务ID
+ */
+ cancelTask: async (taskId) => {
+ const { wsConnection } = useChatBoxStore.getState();
+ if (wsConnection) {
+ wsConnection.send(JSON.stringify({
+ type: 'cancel_task',
+ taskId: taskId
+ }));
+
+ // 乐观更新UI
+ set((state) => ({
+ tasks: state.tasks.map(task =>
+ task.taskId === taskId ? { ...task, status: 'cancelled' } : task
+ )
+ }));
+ }
+ },
+
+ /**
+ * 清理已完成的任务
+ */
+ clearCompleted: () => set((state) => ({
+ tasks: state.tasks.filter(task =>
+ !['completed', 'failed', 'cancelled'].includes(task.status)
+ )
+ })),
+
+ /**
+ * 设置任务列表(用于全量更新)
+ * @param {Array} tasks - 任务列表
+ */
+ setTasks: (tasks) => set({ tasks }),
+
+ /**
+ * 清空所有任务
+ */
+ clearAll: () => set({ tasks: [] })
+}));
+
+export default useTasksStore;
diff --git a/frontend/src/Store/SideBarRight/WorldBookActiveSlice.jsx b/frontend/src/Store/SideBarRight/WorldBookActiveSlice.jsx
new file mode 100644
index 0000000..5708edb
--- /dev/null
+++ b/frontend/src/Store/SideBarRight/WorldBookActiveSlice.jsx
@@ -0,0 +1,37 @@
+import { create } from 'zustand';
+
+/**
+ * 世界书激活显示 Store
+ * 管理当前对话中激活的世界书条目
+ */
+const useWorldBookActiveStore = create((set, get) => ({
+ // ==================== 状态 ====================
+
+ // 激活的条目列表
+ activeEntries: [],
+
+ // ==================== Actions ====================
+
+ /**
+ * 设置激活的条目(覆盖)
+ * @param {Array} entries - 激活的条目列表
+ */
+ setActiveEntries: (entries) => set({
+ activeEntries: entries || []
+ }),
+
+ /**
+ * 添加单个激活条目
+ * @param {Object} entry - 条目对象
+ */
+ addActiveEntry: (entry) => set((state) => ({
+ activeEntries: [...state.activeEntries, entry]
+ })),
+
+ /**
+ * 清空所有激活条目
+ */
+ clearEntries: () => set({ activeEntries: [] })
+}));
+
+export default useWorldBookActiveStore;
diff --git a/frontend/src/components/Mid/ChatBox/ChatBox.css b/frontend/src/components/Mid/ChatBox/ChatBox.css
index cb4ffd7..265db8b 100644
--- a/frontend/src/components/Mid/ChatBox/ChatBox.css
+++ b/frontend/src/components/Mid/ChatBox/ChatBox.css
@@ -12,7 +12,6 @@
flex: 1;
overflow-y: auto;
padding: var(--spacing-lg);
- padding-bottom: 180px; /* 为固定的输入框留出空间 */
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
@@ -330,27 +329,70 @@
position: absolute;
bottom: calc(100% + var(--spacing-sm));
left: 0;
- background: #ffffff;
- border: 1px solid rgba(102, 126, 234, 0.2);
+ background: var(--color-bg-elevated);
+ border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
- padding: var(--spacing-md);
- min-width: 200px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ padding: var(--spacing-sm);
+ min-width: 220px;
z-index: 100;
+ backdrop-filter: blur(10px);
}
+/* 选项组标题 */
+.option-group-title {
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ padding: var(--spacing-xs) var(--spacing-sm);
+ margin-bottom: var(--spacing-xs);
+}
+
+/* 渲染模式按钮 */
+.render-mode-btn {
+ width: 100%;
+ padding: var(--spacing-sm) var(--spacing-md);
+ background: transparent;
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 500;
+ transition: all 0.15s ease;
+ text-align: left;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
+}
+
+.render-mode-btn:hover {
+ background-color: rgba(102, 126, 234, 0.08);
+ border-color: var(--color-accent);
+ color: var(--color-accent);
+}
+
+.render-mode-btn:active {
+ transform: scale(0.98);
+}
+
+/* 复选框选项 - 简化样式 */
.option-checkbox {
display: flex;
align-items: center;
gap: var(--spacing-sm);
- padding: var(--spacing-xs) var(--spacing-sm);
+ padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
- border-radius: var(--radius-sm);
- transition: background-color 0.15s ease;
+ border-radius: var(--radius-md);
+ transition: all 0.15s ease;
+ margin-bottom: var(--spacing-xs);
}
.option-checkbox:hover {
- background-color: rgba(102, 126, 234, 0.05);
+ background-color: rgba(102, 126, 234, 0.06);
}
.option-checkbox input[type="checkbox"] {
@@ -358,15 +400,19 @@
}
.checkmark {
- width: 18px;
- height: 18px;
- border: 2px solid rgba(102, 126, 234, 0.3);
- border-radius: 4px;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--color-border);
+ border-radius: 3px;
position: relative;
- transition: all 0.2s ease;
+ transition: all 0.15s ease;
flex-shrink: 0;
}
+.option-checkbox:hover .checkmark {
+ border-color: var(--color-accent);
+}
+
.option-checkbox input:checked + .checkmark {
background-color: var(--color-accent);
border-color: var(--color-accent);
@@ -375,8 +421,8 @@
.option-checkbox input:checked + .checkmark::after {
content: '';
position: absolute;
- left: 5px;
- top: 2px;
+ left: 4px;
+ top: 1px;
width: 4px;
height: 8px;
border: solid white;
@@ -388,17 +434,18 @@
font-size: 0.85rem;
color: var(--color-text-primary);
user-select: none;
+ flex: 1;
}
+/* 分隔线 */
.option-divider {
height: 1px;
- background: linear-gradient(to right,
- transparent,
- rgba(102, 126, 234, 0.15),
- transparent);
+ background-color: var(--color-border);
margin: var(--spacing-sm) 0;
+ opacity: 0.5;
}
+/* 切换聊天按钮 */
.switch-chat-btn {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
@@ -409,7 +456,7 @@
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
- transition: all 0.2s ease;
+ transition: all 0.15s ease;
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
}
@@ -419,6 +466,10 @@
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
+.switch-chat-btn:active {
+ transform: translateY(0);
+}
+
.chat-input-area {
flex: 1;
min-width: 0;
@@ -584,6 +635,40 @@
padding: var(--spacing-lg);
}
+/* 新建聊天按钮区域 */
+.chat-selector-actions {
+ margin-bottom: var(--spacing-md);
+ padding-bottom: var(--spacing-md);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.new-chat-btn {
+ width: 100%;
+ padding: var(--spacing-md) var(--spacing-lg);
+ background: var(--gradient-primary);
+ color: white;
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-xs);
+}
+
+.new-chat-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+ filter: brightness(1.1);
+}
+
+.new-chat-btn:active {
+ transform: translateY(0);
+}
+
.chat-list {
display: flex;
flex-direction: column;
diff --git a/frontend/src/components/Mid/ChatBox/ChatBox.jsx b/frontend/src/components/Mid/ChatBox/ChatBox.jsx
index 495e5f0..d583c8a 100644
--- a/frontend/src/components/Mid/ChatBox/ChatBox.jsx
+++ b/frontend/src/components/Mid/ChatBox/ChatBox.jsx
@@ -46,7 +46,8 @@ const ChatBox = () => {
sendMessage,
stopGeneration,
options,
- toggleOption
+ toggleOption,
+ cycleRenderMode // ✅ 新增:切换渲染模式
} = useChatBoxStore();
// 点击外部关闭选项面板
@@ -202,6 +203,35 @@ const ChatBox = () => {
alert('切换聊天失败: ' + error.message);
}
};
+
+ // 新建聊天
+ const handleCreateNewChat = async () => {
+ try {
+ const { currentRole, createChat, setChatBoxRoleAndChat } = useChatBoxStore.getState();
+
+ if (!currentRole) {
+ alert('请先选择一个角色');
+ return;
+ }
+
+ // 使用当前时间戳作为聊天名称
+ const chatName = `chat_${Date.now()}`;
+
+ // 创建新聊天
+ await createChat(currentRole, chatName);
+
+ // 切换到新创建的聊天
+ setChatBoxRoleAndChat(currentRole, chatName);
+
+ // 关闭选择器
+ setShowChatSelector(false);
+
+ console.log(`已创建并切换到新聊天: ${chatName}`);
+ } catch (error) {
+ console.error('创建聊天失败:', error);
+ alert('创建聊天失败: ' + error.message);
+ }
+ };
// 渲染单条消息
const renderMessage = (message) => {
@@ -283,14 +313,30 @@ const ChatBox = () => {
) : (
- {options.htmlRender && !isUser ? (
-
- ) : options.markdownRender ? (
+ {options.renderMode === 'html' && !isUser ? (
+ // ✅ 智能 HTML 渲染:检测是否为纯文本,自动转换换行符
+
{
+ // 检测是否包含 HTML 标签
+ const hasHtmlTags = /<[a-z][\s\S]*>/i.test(currentMes);
+ if (hasHtmlTags) {
+ // 如果已有 HTML 标签,直接返回
+ return currentMes;
+ }
+ // 如果是纯文本,将换行符转换为
+ return currentMes
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/\n/g, '
');
+ })()
+ }} />
+ ) : options.renderMode === 'markdown' ? (
) : (
{currentMes}
)}
- {hasSwipes && isLatestMessage && !isUser && (
+ {hasSwipes && !isUser && isLatestMessage && (
{showOptions && (
+ {/* 新建聊天按钮 */}
+
+
+
+
{characterChats.length > 0 ? (
{characterChats.map((chat, index) => (
diff --git a/frontend/src/components/SideBarLeft/SideBarLeft.css b/frontend/src/components/SideBarLeft/SideBarLeft.css
index 2968f42..d2c84f1 100644
--- a/frontend/src/components/SideBarLeft/SideBarLeft.css
+++ b/frontend/src/components/SideBarLeft/SideBarLeft.css
@@ -47,10 +47,29 @@
.sidebar-content {
flex: 1;
overflow-y: auto;
- padding: var(--spacing-lg);
+ padding: 0; /* ✅ 移除padding,滚动条作为间隔 */
min-height: 0;
}
+/* ✅ 美化滚动条 - 符合主题 */
+.sidebar-content::-webkit-scrollbar {
+ width: 8px;
+}
+
+.sidebar-content::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.sidebar-content::-webkit-scrollbar-thumb {
+ background: var(--color-scrollbar);
+ border-radius: 4px;
+ transition: all 0.2s ease;
+}
+
+.sidebar-content::-webkit-scrollbar-thumb:hover {
+ background: var(--color-scrollbar-hover);
+}
+
/* Tab placeholder for empty states */
.tab-placeholder {
padding: var(--spacing-lg);
diff --git a/frontend/src/components/SideBarLeft/SideBarLeft.jsx b/frontend/src/components/SideBarLeft/SideBarLeft.jsx
index 4051543..2499b7c 100644
--- a/frontend/src/components/SideBarLeft/SideBarLeft.jsx
+++ b/frontend/src/components/SideBarLeft/SideBarLeft.jsx
@@ -3,15 +3,18 @@ import React from 'react';
import './SideBarLeft.css';
import { useSideBarLeftStore } from '../../Store/indexStore';
import usePresetStore from '../../Store/SideBarLeft/PresetSlice';
+import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice';
import Gallery from './tabs/Gallery';
import CharacterCard from './tabs/CharacterCard';
import ApiConfig from './tabs/ApiConfig';
import Presets from './tabs/Presets';
import WorldBook from './tabs/WorldBook';
+import TokenUsage from './tabs/TokenUsage/TokenUsage';
const SideBarLeft = () => {
const { activeTab, tabs, setActiveTab } = useSideBarLeftStore();
const { fetchPresets } = usePresetStore();
+ const { fetchProfiles } = useApiConfigStore();
// 处理标签切换
const handleTabClick = (tabId) => {
@@ -21,6 +24,11 @@ const SideBarLeft = () => {
if (tabId === 'presets') {
fetchPresets();
}
+
+ // 如果切换到API配置标签,刷新配置文件列表
+ if (tabId === 'api') {
+ fetchProfiles();
+ }
};
return (
@@ -44,6 +52,7 @@ const SideBarLeft = () => {
{activeTab === 'api' &&
}
{activeTab === 'presets' &&
}
{activeTab === 'worldbook' &&
}
+ {activeTab === 'tokenUsage' &&
}
);
diff --git a/frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.jsx b/frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.jsx
index 74e5f96..8541662 100644
--- a/frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.jsx
+++ b/frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.jsx
@@ -6,7 +6,7 @@ import ComfyUIWorkflowManager from './ComfyUIWorkflowManager';
import './ApiConfig.css';
const ApiConfig = () => {
- // 从store中获取状态和方法
+ // 从 store中获取状态和方法
const {
profiles,
currentProfile,
@@ -77,21 +77,11 @@ const ApiConfig = () => {
// 跟踪哪些 API 有修改
const [modifiedApis, setModifiedApis] = useState({});
+ // 跟踪用户是否正在编辑 API Key(用于控制脱敏显示)
+ const [isEditingApiKey, setIsEditingApiKey] = useState(false);
+
// 获取当前激活的分页
const { activeTab } = useSideBarLeftStore();
-
- // 记录上一次的分页状态
- const prevActiveTabRef = React.useRef(activeTab);
-
- // 组件加载时获取配置文件列表 - 只在切换到API分页时刷新
- useEffect(() => {
- // 检测是否从其他分页切换到API分页
- if (activeTab === 'api' && prevActiveTabRef.current !== 'api') {
- fetchProfiles();
- }
- // 更新上一次的分页状态
- prevActiveTabRef.current = activeTab;
- }, [activeTab, fetchProfiles]);
// 当选中配置文件时,加载该配置
useEffect(() => {
@@ -100,6 +90,15 @@ const ApiConfig = () => {
}
}, [selectedProfileId]);
+ // 当 profiles 加载完成后,如果没有选中的配置文件,则自动加载第一个
+ useEffect(() => {
+ if (profiles.length > 0 && !selectedProfileId && activeTab === 'api') {
+ // 尝试加载 activeMap 中记录的配置文件,如果没有则加载第一个
+ const defaultProfileId = activeMap[currentCategory] || profiles[0].id;
+ setSelectedProfileId(defaultProfileId);
+ }
+ }, [profiles, selectedProfileId, activeTab, currentCategory, activeMap]);
+
// 加载配置文件
const loadProfile = async (profileId) => {
try {
@@ -123,6 +122,11 @@ const ApiConfig = () => {
const { name, value, type, checked } = e.target;
const newValue = type === 'checkbox' ? checked : value;
+ // 如果是 API Key 字段,标记为正在编辑
+ if (name === 'apiKey') {
+ setIsEditingApiKey(true);
+ }
+
if (path) {
// 嵌套路径更新,例如: ['imageModel', 'local', 'apiUrl']
setFormData(prev => {
@@ -267,8 +271,10 @@ const ApiConfig = () => {
return;
}
- if (!currentApi.apiKey) {
- alert('请先填写 API 密钥');
+ // 检查 API Key:可以为空(如果已保存过,后端会使用保存的 key)
+ // 但如果既没有输入 key,也没有保存过,则会失败
+ if (!currentApi.apiKey || currentApi.apiKey.trim() === '') {
+ alert('请先填写 API 密钥,或先保存配置文件');
return;
}
@@ -277,6 +283,7 @@ const ApiConfig = () => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+ id: selectedProfileId || null, // 使用 id 字段传递 profileId
apiUrl: currentApi.apiUrl,
apiKey: currentApi.apiKey,
category: currentCategory,
@@ -388,19 +395,9 @@ const ApiConfig = () => {
if (selectedProfileId && confirm('确定要删除此配置文件吗?')) {
await deleteProfile(selectedProfileId);
setSelectedProfileId('');
- handleCreateNew();
}
};
- // 处理设为默认配置
- const handleSetActive = async () => {
- if (!selectedProfileId) {
- alert('请先保存配置文件');
- return;
- }
- await setActiveConfig(currentCategory, selectedProfileId);
- };
-
// 配置区域标签(简短名称 + Tooltip)
const configTabs = [
{ id: 'mainLLM', label: '核心', tooltip: '主 LLM 模型 - 用于主要对话和推理' },
@@ -409,8 +406,13 @@ const ApiConfig = () => {
{ id: 'ragEmbedding', label: '向量', tooltip: 'RAG 嵌入模型 - 用于文本向量化和检索' }
];
- // 判断当前 API 密钥是否是脱敏的
- const isApiKeyMasked = formData[currentCategory].apiKey && formData[currentCategory].apiKey.endsWith('****');
+ // 脱敏 API Key 显示(只显示最后6位)
+ const maskApiKey = (apiKey) => {
+ if (!apiKey || apiKey.length <= 6) {
+ return apiKey || '';
+ }
+ return '•'.repeat(apiKey.length - 6) + apiKey.slice(-6);
+ };
// 计算有修改的 API 数量
const modifiedCount = Object.keys(modifiedApis).filter(k => modifiedApis[k]).length;
@@ -438,41 +440,38 @@ const ApiConfig = () => {
onChange={handleSelectProfile}
className="form-control profile-select-input"
>
-
- {profiles.map(profile => (
-
- ))}
-
-
- {!selectedProfileId ? (
-
+ {profiles.length === 0 ? (
+
) : (
<>
-
-
+
+ {profiles.map(profile => (
+
+ ))}
>
)}
+
+
+
+ {selectedProfileId && (
+
+ )}
@@ -606,13 +605,31 @@ const ApiConfig = () => {
handleChange(e, ['imageModel', 'cloud', 'apiKey'])}
- placeholder="sk-..."
+ onBlur={() => setIsEditingApiKey(false)}
+ onFocus={() => {
+ if (formData.imageModel.cloud.apiKey && !isEditingApiKey) {
+ // 聚焦时清空,让用户重新输入
+ setFormData(prev => ({
+ ...prev,
+ imageModel: {
+ ...prev.imageModel,
+ cloud: {
+ ...prev.imageModel.cloud,
+ apiKey: ''
+ }
+ }
+ }));
+ }
+ setIsEditingApiKey(true);
+ }}
+ placeholder="sk-...(输入新密钥将覆盖旧密钥)"
className="form-control"
/>
+ 首次输入可见,之后仅显示最后6位
@@ -668,17 +685,29 @@ const ApiConfig = () => {
setIsEditingApiKey(false)}
+ onFocus={() => {
+ if (formData[currentCategory].apiKey && !isEditingApiKey) {
+ // 聚焦时清空,让用户重新输入
+ setFormData(prev => ({
+ ...prev,
+ [currentCategory]: {
+ ...prev[currentCategory],
+ apiKey: ''
+ }
+ }));
+ }
+ setIsEditingApiKey(true);
+ }}
+ placeholder="sk-...(输入新密钥将覆盖旧密钥)"
className="form-control"
/>
- {isApiKeyMasked && (
- 当前密钥已加密存储
- )}
+ 首次输入可见,之后仅显示最后6位
diff --git a/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.css b/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.css
index f267d5f..5a1f27b 100644
--- a/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.css
+++ b/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.css
@@ -69,6 +69,9 @@
cursor: pointer;
font-size: 10px;
transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
}
.tag-btn:hover {
@@ -76,12 +79,36 @@
color: var(--color-accent);
}
-.tag-btn.active {
- background: var(--color-accent);
- border-color: var(--color-accent);
+/* 包含模式(第一次点击)- 绿色 */
+.tag-btn.tag-include {
+ background: #10b981;
+ border-color: #10b981;
color: white;
}
+.tag-btn.tag-include:hover {
+ background: #059669;
+ border-color: #059669;
+}
+
+/* 排除模式(第二次点击)- 红色 */
+.tag-btn.tag-exclude {
+ background: #ef4444;
+ border-color: #ef4444;
+ color: white;
+}
+
+.tag-btn.tag-exclude:hover {
+ background: #dc2626;
+ border-color: #dc2626;
+}
+
+/* 排除前缀符号 */
+.tag-prefix {
+ font-weight: bold;
+ font-size: 12px;
+}
+
/* 错误和加载提示 */
.error-message,
.loading-message,
@@ -270,47 +297,116 @@
flex-direction: column;
gap: 8px;
padding: 8px;
+ position: relative; /* 为悬浮工具栏提供定位上下文 */
}
-.edit-header {
+/* 悬浮工具栏 */
+.floating-toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ margin-bottom: 8px;
+}
+
+/* 工具栏内容(默认隐藏) */
+.toolbar-content {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 8px;
+ padding: 6px 10px;
background: var(--color-bg-tertiary);
- border-radius: 4px;
+ border-radius: 6px;
border: 1px solid var(--color-border-light);
- flex-wrap: wrap; /* 允许换行 */
- gap: 6px; /* 添加间距 */
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+
+ /* 默认状态:收缩 */
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+ transform: scale(0.8);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* 悬停时展开 */
+.floating-toolbar:hover .toolbar-content {
+ max-height: 60px;
+ opacity: 1;
+ transform: scale(1);
+}
+
+.toolbar-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 0; /* 允许收缩 */
}
.edit-title {
font-size: 12px;
font-weight: 600;
color: var(--color-text-primary);
- flex: 1; /* 占据剩余空间 */
- min-width: 120px; /* 最小宽度,防止过度压缩 */
- white-space: nowrap; /* 不换行 */
- overflow: hidden; /* 超出隐藏 */
- text-overflow: ellipsis; /* 显示省略号 */
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.edit-actions {
+/* 小球指示器 */
+.toolbar-indicator {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 36px;
+ height: 36px;
+ background: var(--color-accent);
+ border-radius: 50%;
display: flex;
- gap: 6px;
align-items: center;
- flex-wrap: wrap; /* 按钮也可以换行 */
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ transform: translateY(-50%) scale(1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ }
+ 50% {
+ transform: translateY(-50%) scale(1.1);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ }
+}
+
+.floating-toolbar:hover .toolbar-indicator {
+ opacity: 0;
+ transform: translateY(-50%) scale(0);
+}
+
+.indicator-icon {
+ font-size: 18px;
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
+}
+
+.toolbar-actions {
+ display: flex;
+ gap: 4px;
+ align-items: center;
}
.export-format-selector {
- padding: 4px 8px;
+ padding: 3px 6px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 11px;
cursor: pointer;
- transition: border-color 0.15s ease;
+ transition: all 0.15s ease;
}
.export-format-selector:hover {
@@ -322,55 +418,66 @@
border-color: var(--color-accent);
}
-.edit-btn {
- padding: 4px 10px;
+.toolbar-btn {
+ width: 28px;
+ height: 28px;
+ padding: 0;
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
- font-size: 11px;
+ font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-bg-primary);
}
-.edit-btn.save {
+.toolbar-btn:hover {
+ transform: scale(1.1);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+.toolbar-btn.save {
background: var(--color-accent);
border-color: var(--color-accent);
color: white;
}
-.edit-btn.save:hover {
+.toolbar-btn.save:hover {
background: var(--color-accent-dark);
border-color: var(--color-accent-dark);
}
-.edit-btn.export {
+.toolbar-btn.export {
background: #10b981;
border-color: #10b981;
color: white;
}
-.edit-btn.export:hover {
+.toolbar-btn.export:hover {
background: #059669;
border-color: #059669;
}
-.edit-btn.delete {
+.toolbar-btn.delete {
background: #ef4444;
border-color: #ef4444;
color: white;
}
-.edit-btn.delete:hover {
+.toolbar-btn.delete:hover {
background: #dc2626;
border-color: #dc2626;
}
-.edit-btn.cancel {
+.toolbar-btn.cancel {
background: var(--color-bg-primary);
color: var(--color-text-secondary);
}
-.edit-btn.cancel:hover {
+.toolbar-btn.cancel:hover {
background: var(--color-error);
border-color: var(--color-error);
color: white;
@@ -415,6 +522,9 @@
.form-group textarea {
resize: vertical;
min-height: 60px;
+ max-height: 300px; /* 设置最大高度 */
+ overflow-y: auto;
+ transition: height 0.2s ease; /* 平滑过渡 */
}
.form-hint {
diff --git a/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.jsx b/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.jsx
index 2595be3..89efc60 100644
--- a/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.jsx
+++ b/frontend/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.jsx
@@ -2,6 +2,7 @@
import React, { useState, useEffect, useRef, useCallback, memo } from 'react'; // ✅ 保留 useState 用于 exportFormat
import { useCharacterStore, useCharacterCardUIStore } from '../../../../Store/SideBarLeft'; // ✅ 新增
import useChatBoxStore from '../../../../Store/Mid/ChatBoxSlice';
+import useWorldBookStore from '../../../../Store/SideBarLeft/WorldBookSlice'; // 引入世界书 Store
import './CharacterCard.css';
// 优化的角色卡片项组件 - 使用 memo 和 useCallback
@@ -54,8 +55,8 @@ const CharacterItem = memo(({ character, isSelected, onSelect }) => {
)}
{character.tags && character.tags.length > 0 && (
- {character.tags.slice(0, 3).map(tag => (
- {tag}
+ {character.tags.slice(0, 3).map((tag, index) => (
+ {tag}
))}
)}
@@ -86,13 +87,14 @@ const CharacterCard = () => {
// ✅ 从 CharacterCardUIStore 获取 UI 状态
const {
- filterTag,
+ filterTags,
isEditing,
editForm,
currentPage,
pageSize,
- setFilterTag,
- clearFilter,
+ toggleFilterTag,
+ removeFilterTag,
+ clearAllFilters,
startEditing,
cancelEditing,
updateEditForm,
@@ -102,13 +104,17 @@ const CharacterCard = () => {
prevPage
} = useCharacterCardUIStore();
+ // 引入世界书 Store
+ const { worldBooks, fetchWorldBooks } = useWorldBookStore();
+
const [exportFormat, setExportFormat] = useState('png'); // 导出格式: 'png' 或 'json'
const fileInputRef = useRef(null);
const clickTimeoutRef = useRef(null);
- // 加载角色列表
+ // 加载角色列表和世界书列表
useEffect(() => {
fetchCharacters();
+ fetchWorldBooks(); // 加载世界书列表
// 清理函数
return () => {
@@ -121,10 +127,31 @@ const CharacterCard = () => {
// 获取所有唯一的 tags
const allTags = [...new Set(characters.flatMap(char => char.tags || []))];
- // 过滤角色
- const filteredCharacters = filterTag
- ? characters.filter(char => (char.tags || []).includes(filterTag))
- : characters;
+ // 过滤角色(支持多标签交集筛选)
+ const filteredCharacters = React.useMemo(() => {
+ if (filterTags.length === 0) return characters;
+
+ // 分离包含和排除标签
+ const includeTags = filterTags
+ .filter(f => f.startsWith('include:'))
+ .map(f => f.substring(8));
+
+ const excludeTags = filterTags
+ .filter(f => f.startsWith('exclude:'))
+ .map(f => f.substring(8));
+
+ return characters.filter(char => {
+ const charTags = char.tags || [];
+
+ // 检查是否包含所有必须的标签(交集)
+ const hasAllInclude = includeTags.every(tag => charTags.includes(tag));
+
+ // 检查是否不包含所有排除的标签
+ const hasNoExclude = excludeTags.every(tag => !charTags.includes(tag));
+
+ return hasAllInclude && hasNoExclude;
+ });
+ }, [characters, filterTags]);
// ✅ 计算当前页和总页数
const totalPages = Math.ceil(filteredCharacters.length / pageSize) || 1;
@@ -314,6 +341,13 @@ const CharacterCard = () => {
updateEditForm(field, value); // ✅ 使用 store 方法
};
+ // 自动调整 textarea 高度
+ const autoResizeTextarea = (e) => {
+ const textarea = e.target;
+ textarea.style.height = 'auto'; // 重置高度
+ textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px'; // 设置新高度,最大300px
+ };
+
return (
{/* 标题栏 */}
@@ -338,20 +372,93 @@ const CharacterCard = () => {
{allTags.length > 0 && (
- {allTags.map(tag => (
-
- ))}
+ {allTags.map(tag => {
+ // 判断当前标签的状态
+ let status = 'none'; // none, include, exclude
+ const includeFilter = `include:${tag}`;
+ const excludeFilter = `exclude:${tag}`;
+
+ if (filterTags.includes(includeFilter)) {
+ status = 'include';
+ } else if (filterTags.includes(excludeFilter)) {
+ status = 'exclude';
+ }
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ {/* 当前筛选条件显示 */}
+ {filterTags.length > 0 && (
+
+
+ 筛选中:
+
+ {filterTags.map((filter) => {
+ const mode = filter.startsWith('include:') ? 'include' : 'exclude';
+ const tag = filter.substring(filter.indexOf(':') + 1);
+ return (
+
+ {mode === 'exclude' && '¬'}
+ {tag}
+
+
+ );
+ })}
)}
@@ -372,21 +479,39 @@ const CharacterCard = () => {
{/* 编辑模式 */}
{isEditing && selectedCharacter && editForm && (
-
-
编辑角色: {selectedCharacter.name}
-
-
-
-
-
-
+ {/* 悬浮工具栏 */}
+
+
+
+ 编辑: {selectedCharacter.name}
+
+
+
+
+
+
+
+
+
+ {/* 小球指示器 */}
+
+ ⚙️
@@ -399,13 +524,18 @@ const CharacterCard = () => {
onChange={(e) => handleFormChange('name', e.target.value)}
placeholder="角色名称"
/>
+
💡 提示:修改角色名将同步重命名文件夹和所有相关引用
-
+
+
+ 💡 选择后,该角色将自动加载绑定的世界书内容
+
+
+
+
`${k}:${v}`).join(';')}
onChange={(e) => {
- // 支持多种分隔符:逗号、分号、空格
- const tagsList = e.target.value
- .split(/[,;\s]+/)
- .map(tag => tag.trim())
- .filter(tag => tag !== '');
- handleFormChange('tags', tagsList);
+ // 解析格式:key1:value1;key2:value2,key3:value3
+ // 支持分隔符:分号、逗号
+ const entries = e.target.value
+ .split(/[;,]/)
+ .map(pair => pair.trim())
+ .filter(pair => pair !== '')
+ .map(pair => {
+ const [key, ...valueParts] = pair.split(':');
+ const value = valueParts.join(':').trim(); // 支持值中包含冒号
+ return { key: key.trim(), value };
+ })
+ .filter(({ key, value }) => key && value !== undefined);
+
+ // 构建 tableDefaults 对象
+ const tableDefaults = {};
+ const tableHeaders = [];
+
+ entries.forEach(({ key, value }) => {
+ tableHeaders.push(key);
+ // 尝试转换为数字
+ const numValue = Number(value);
+ tableDefaults[key] = isNaN(numValue) ? value : numValue;
+ });
+
+ handleFormChange('tableHeaders', tableHeaders);
+ handleFormChange('tableDefaults', tableDefaults);
}}
- placeholder="力量:80, 敏捷:65, 智力:90, HP:100"
+ placeholder="力量:80;敏捷:65;智力:90;HP:100"
/>
- 每个标签是一个键值对,用逗号/分号/空格分隔。双击右侧表格中的标签可编辑。
+ 格式:key:value,用分号或逗号分隔。例如:力量:80;敏捷:65,HP:100
@@ -488,7 +670,7 @@ const CharacterCard = () => {
{currentPageCharacters.length === 0 ? (
- {filterTag ? '没有符合筛选的角色' : '暂无角色,请导入或创建'}
+ {filterTags.length > 0 ? '没有符合筛选的角色' : '暂无角色,请导入或创建'}
) : (
currentPageCharacters.map(char => (
diff --git a/frontend/src/components/SideBarLeft/tabs/Presets/Presets.css b/frontend/src/components/SideBarLeft/tabs/Presets/Presets.css
index 46d1092..5fb9afd 100644
--- a/frontend/src/components/SideBarLeft/tabs/Presets/Presets.css
+++ b/frontend/src/components/SideBarLeft/tabs/Presets/Presets.css
@@ -108,7 +108,7 @@
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: var(--shadow-lg);
- z-index: 100;
+ z-index: 1100; /* ✅ 高于 ChatBox (z-index: 1000) */
min-width: 140px;
padding: 4px;
}
diff --git a/frontend/src/components/SideBarLeft/tabs/Presets/Presets.jsx b/frontend/src/components/SideBarLeft/tabs/Presets/Presets.jsx
index 4e78ca0..da1ec85 100644
--- a/frontend/src/components/SideBarLeft/tabs/Presets/Presets.jsx
+++ b/frontend/src/components/SideBarLeft/tabs/Presets/Presets.jsx
@@ -77,6 +77,7 @@ const PresetPanel = () => {
top_k: "随机采样范围,从概率最高的K个词中选择",
max_ctx: "上下文窗口大小(Token上限)",
max_tokens: "单次回复的最大长度",
+ request_timeout: "LLM请求超时时间(秒)",
seed: "随机种子(-1为随机)",
n: "生成回复的数量"
};
@@ -90,6 +91,7 @@ const PresetPanel = () => {
top_k: "Top-K",
max_ctx: "上下文",
max_tokens: "MaxTok",
+ request_timeout: "超时",
seed: "种子",
n: "数量"
};
@@ -114,7 +116,7 @@ const PresetPanel = () => {
let convertedValue = value;
if (name === 'temperature' || name === 'freq_penalty' || name === 'pres_penalty' || name === 'top_p') {
convertedValue = parseFloat(value);
- } else if (name === 'top_k' || name === 'max_ctx' || name === 'max_tokens' || name === 'seed' || name === 'n') {
+ } else if (name === 'top_k' || name === 'max_ctx' || name === 'max_tokens' || name === 'request_timeout' || name === 'seed' || name === 'n') {
convertedValue = parseInt(value, 10);
}
@@ -186,7 +188,8 @@ const PresetPanel = () => {
top_k: importedPreset.topK,
frequency_penalty: importedPreset.frequencyPenalty,
presence_penalty: importedPreset.presencePenalty,
- max_tokens: importedPreset.maxLength
+ max_tokens: importedPreset.maxLength,
+ request_timeout: importedPreset.requestTimeout || 60
};
// 转换 entries 为 promptComponents 格式
@@ -213,7 +216,8 @@ const PresetPanel = () => {
presence_penalty: importedPreset.presence_penalty,
top_p: importedPreset.top_p,
top_k: importedPreset.top_k,
- max_tokens: importedPreset.max_tokens || importedPreset.openai_max_tokens
+ max_tokens: importedPreset.max_tokens || importedPreset.openai_max_tokens,
+ request_timeout: importedPreset.request_timeout || 60
};
importedComponents = importedPreset.promptComponents || [];
}
@@ -261,6 +265,7 @@ const PresetPanel = () => {
frequencyPenalty: parameters.frequency_penalty,
presencePenalty: parameters.presence_penalty,
maxLength: parameters.max_tokens,
+ requestTimeout: parameters.request_timeout || 60,
isDefault: false,
// PromptPresetView 部分
@@ -744,6 +749,25 @@ const PresetPanel = () => {
/>
+ {/* 请求超时输入框 */}
+
+
+ handleParameterChange('request_timeout', e.target.value)}
+ className="parameter-input-compact"
+ />
+
+
{/* 随机种子输入框 */}
@@ -130,7 +132,7 @@ const SideBarRight = () => {
>
{(() => {
const tab = allTabs.find(t => t.id === selectedTabs[1]);
- return tab.component ?
:
{tab.label}内容
;
+ return tab && tab.component ?
:
{tab?.label || '未知'}内容
;
})()}
>
diff --git a/frontend/src/components/SideBarRight/tabs/Debug/Debug.jsx b/frontend/src/components/SideBarRight/tabs/Debug/Debug.jsx
deleted file mode 100644
index e4639d8..0000000
--- a/frontend/src/components/SideBarRight/tabs/Debug/Debug.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-const Debug = () => {
- return (
-
-
上下文调试
-
这是上下文调试面板的占位页面
-
- );
-};
-
-export default Debug;
diff --git a/frontend/src/components/SideBarRight/tabs/Debug/index.js b/frontend/src/components/SideBarRight/tabs/Debug/index.js
deleted file mode 100644
index c07a8f0..0000000
--- a/frontend/src/components/SideBarRight/tabs/Debug/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './Debug';
diff --git a/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.css b/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.css
new file mode 100644
index 0000000..706fb88
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.css
@@ -0,0 +1,152 @@
+.tasks-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: var(--color-bg-secondary);
+}
+
+.tab-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ background: var(--color-bg-tertiary);
+ border-bottom: 1px solid var(--color-border);
+ min-height: 36px;
+}
+
+.title-text {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.task-count {
+ font-size: 11px;
+ color: var(--color-text-muted);
+ background: var(--color-bg-secondary);
+ padding: 2px 6px;
+ border-radius: 10px;
+ min-width: 20px;
+ text-align: center;
+}
+
+.btn-clear {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ transition: all 0.2s;
+ opacity: 0.6;
+}
+
+.btn-clear:hover {
+ opacity: 1;
+ background: var(--color-bg-hover);
+}
+
+.empty-state {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-text-muted);
+ font-size: 12px;
+}
+
+.tasks-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.task-item {
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border-light);
+ border-radius: 4px;
+ padding: 6px 8px;
+ transition: all 0.2s;
+}
+
+.task-item:hover {
+ background: var(--color-bg-hover);
+ border-color: var(--color-border);
+}
+
+.task-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 4px;
+}
+
+.task-icon {
+ font-size: 14px;
+ line-height: 1;
+}
+
+.task-name {
+ flex: 1;
+ font-size: 12px;
+ color: var(--color-text-primary);
+ font-weight: 500;
+}
+
+.task-status-badge {
+ font-size: 10px;
+ white-space: nowrap;
+}
+
+.status-pending {
+ color: var(--color-text-muted);
+}
+
+.status-running {
+ color: var(--color-primary);
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+.status-completed {
+ color: var(--color-success);
+}
+
+.status-failed {
+ color: var(--color-error);
+}
+
+.status-cancelled {
+ color: var(--color-text-muted);
+}
+
+.task-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 4px;
+}
+
+.btn-cancel-task {
+ font-size: 10px;
+ padding: 2px 8px;
+ background: var(--color-error);
+ color: white;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: all 0.2s;
+ opacity: 0.9;
+}
+
+.btn-cancel-task:hover {
+ opacity: 1;
+ transform: scale(1.05);
+}
diff --git a/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.jsx b/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.jsx
new file mode 100644
index 0000000..c49a079
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/Tasks/Tasks.jsx
@@ -0,0 +1,73 @@
+// frontend/src/components/SideBarRight/tabs/Tasks/Tasks.jsx
+import React from 'react';
+import './Tasks.css';
+import useTasksStore from '../../../../Store/SideBarRight/TasksSlice';
+
+const Tasks = () => {
+ const { tasks, clearCompleted, cancelTask } = useTasksStore();
+
+ if (tasks.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ 任务队列
+ {tasks.length}
+
+
+
+
+ {tasks.map(task => (
+
+
+
+ {task.taskType === 'image_workflow' ? '🖼️' : '📊'}
+
+
+ {task.taskType === 'image_workflow' ? '生图任务' : '表格维护'}
+
+
+ {task.status === 'pending' && ⏸️ 等待}
+ {task.status === 'running' && ⏳ 进行中}
+ {task.status === 'completed' && ✓ 完成}
+ {task.status === 'failed' && ✗ 失败}
+ {task.status === 'cancelled' && ⊘ 取消}
+
+
+
+ {task.status === 'running' && (
+
+
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default Tasks;
diff --git a/frontend/src/components/SideBarRight/tabs/Tasks/index.js b/frontend/src/components/SideBarRight/tabs/Tasks/index.js
new file mode 100644
index 0000000..40e8ad4
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/Tasks/index.js
@@ -0,0 +1 @@
+export { default } from './Tasks';
diff --git a/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.css b/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.css
new file mode 100644
index 0000000..f16cf56
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.css
@@ -0,0 +1,152 @@
+.worldbook-active-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: var(--color-bg-secondary);
+}
+
+.tab-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ background: var(--color-bg-tertiary);
+ border-bottom: 1px solid var(--color-border);
+ min-height: 36px;
+}
+
+.title-text {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.entry-count {
+ font-size: 11px;
+ color: var(--color-text-muted);
+ background: var(--color-bg-secondary);
+ padding: 2px 6px;
+ border-radius: 10px;
+ min-width: 20px;
+ text-align: center;
+}
+
+.btn-clear {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ transition: all 0.2s;
+ opacity: 0.6;
+}
+
+.btn-clear:hover {
+ opacity: 1;
+ background: var(--color-bg-hover);
+}
+
+.empty-state {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-text-muted);
+ font-size: 12px;
+}
+
+.active-entries-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.position-group {
+ margin-bottom: 4px;
+}
+
+.position-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 6px;
+ background: var(--color-bg-tertiary);
+ border-radius: 3px;
+ margin-bottom: 4px;
+ font-size: 11px;
+}
+
+.position-number {
+ font-weight: 600;
+ color: var(--color-primary);
+}
+
+.position-name {
+ flex: 1;
+ color: var(--color-text-primary);
+}
+
+.position-count {
+ color: var(--color-text-muted);
+ font-size: 10px;
+}
+
+.active-entry-item {
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border-light);
+ border-radius: 4px;
+ padding: 6px 8px;
+ margin-bottom: 4px;
+ transition: all 0.2s;
+}
+
+.active-entry-item:hover {
+ background: var(--color-bg-hover);
+ border-color: var(--color-border);
+}
+
+.entry-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ font-size: 10px;
+}
+
+.entry-uid {
+ color: var(--color-text-muted);
+ font-family: monospace;
+}
+
+.entry-depth {
+ color: var(--color-accent);
+}
+
+.entry-keywords {
+ margin-bottom: 4px;
+}
+
+.keyword-tag {
+ display: inline-block;
+ font-size: 10px;
+ padding: 2px 6px;
+ background: rgba(var(--color-primary-rgb), 0.1);
+ border: 1px solid var(--color-primary);
+ border-radius: 3px;
+ color: var(--color-primary);
+}
+
+.trigger-type {
+ font-size: 10px;
+ color: var(--color-text-muted);
+}
+
+.entry-preview {
+ font-size: 11px;
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+ word-break: break-word;
+}
diff --git a/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.jsx b/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.jsx
new file mode 100644
index 0000000..3c96dad
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.jsx
@@ -0,0 +1,101 @@
+// frontend-react/src/components/SideBarRight/tabs/WorldBookActive/WorldBookActive.jsx
+import React from 'react';
+import './WorldBookActive.css';
+import useWorldBookActiveStore from '../../../../Store/SideBarRight/WorldBookActiveSlice';
+
+const WorldBookActive = () => {
+ const { activeEntries, clearEntries } = useWorldBookActiveStore();
+
+ if (activeEntries.length === 0) {
+ return (
+
+ );
+ }
+
+ // 按位置分组显示
+ const groupedEntries = activeEntries.reduce((acc, entry) => {
+ const pos = entry.position || 0;
+ if (!acc[pos]) {
+ acc[pos] = [];
+ }
+ acc[pos].push(entry);
+ return acc;
+ }, {});
+
+ // 位置标签映射
+ const positionLabels = {
+ 0: '世界书之前',
+ 1: '世界书之后',
+ 2: '示例对话之前',
+ 3: '示例对话之后',
+ 4: '作者注释(顶部)',
+ 5: '作者注释(底部)',
+ 6: '深度插入',
+ 7: '宏替换'
+ };
+
+ return (
+
+ {/* 标题栏 */}
+
+ 世界书激活
+ {activeEntries.length}
+
+
+
+ {/* 激活条目列表 */}
+
+ {Object.entries(groupedEntries).map(([position, entries]) => (
+
+
+ Pos {position}
+ {positionLabels[position] || `位置 ${position}`}
+ {entries.length} 条
+
+
+ {entries.map((entry, index) => (
+
+
+ UID: {entry.uid}
+ Depth: {entry.depth || 0}
+
+
+ {entry.triggerKeywords && entry.triggerKeywords.length > 0 ? (
+
+ 🔑 {entry.triggerKeywords.join(', ')}
+
+ ) : (
+
+ {entry.triggerType === 'constant' ? '📌 常驻' :
+ entry.triggerType === 'condition' ? '⚙️ 条件' : '❓ 未知'}
+
+ )}
+
+
+ {entry.contentPreview || entry.content?.substring(0, 100) || '(无内容)'}
+ {entry.content?.length > 100 ? '...' : ''}
+
+
+ ))}
+
+ ))}
+
+
+ );
+};
+
+export default WorldBookActive;
diff --git a/frontend/src/components/SideBarRight/tabs/WorldBookActive/index.js b/frontend/src/components/SideBarRight/tabs/WorldBookActive/index.js
new file mode 100644
index 0000000..1e2b1d0
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/WorldBookActive/index.js
@@ -0,0 +1 @@
+export { default } from './WorldBookActive';
diff --git a/frontend/src/components/TopBar/TopBar.css b/frontend/src/components/TopBar/TopBar.css
index 4b34983..121e98c 100644
--- a/frontend/src/components/TopBar/TopBar.css
+++ b/frontend/src/components/TopBar/TopBar.css
@@ -330,3 +330,160 @@
display: flex;
overflow: hidden;
}
+
+/* ==================== 设置面板专用样式 ==================== */
+
+.settings-panel .panel-body {
+ overflow-y: auto;
+ max-height: calc(100vh - 200px);
+}
+
+.settings-tabs {
+ display: flex;
+ gap: 8px;
+ padding: 0 16px;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.settings-tabs .tab-button {
+ padding: 8px 16px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+}
+
+.settings-tabs .tab-button:hover {
+ color: var(--color-text-primary);
+ background-color: var(--color-bg-tertiary);
+}
+
+.settings-tabs .tab-button.active {
+ color: white;
+ border-bottom-color: var(--color-accent);
+ background-color: var(--color-accent);
+}
+
+.settings-section {
+ animation: fadeIn 0.3s ease-in;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.message {
+ padding: 12px;
+ margin-bottom: 16px;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.message.success {
+ background: rgba(76, 175, 80, 0.1);
+ border: 1px solid #4caf50;
+ color: #4caf50;
+}
+
+.message.error {
+ background: rgba(244, 67, 54, 0.1);
+ border: 1px solid #f44336;
+ color: #f44336;
+}
+
+.btn-primary {
+ padding: 10px 20px;
+ background: var(--color-accent);
+ border: none;
+ border-radius: 4px;
+ color: white;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--color-accent-hover, #0056b3);
+ transform: translateY(-1px);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* 设置面板输入框 */
+.settings-input {
+ width: 100%;
+ padding: 8px 12px;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: 4px;
+ color: var(--color-text-primary);
+ font-size: 14px;
+ box-sizing: border-box;
+ transition: all 0.2s;
+}
+
+.settings-input:focus {
+ outline: none;
+ border-color: var(--color-accent);
+ background: var(--color-bg-secondary);
+}
+
+.settings-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+/* 设置面板代码块 */
+.settings-code {
+ display: block;
+ padding: 8px 12px;
+ background: var(--color-bg-tertiary);
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ font-size: 13px;
+ color: var(--color-accent);
+ margin-bottom: 8px;
+}
+
+/* 设置面板文件列表 */
+.settings-file-list {
+ margin: 0;
+ padding-left: 20px;
+ list-style: none;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+}
+
+.settings-file-list li {
+ padding: 4px 0;
+}
+
+/* 设置面板提示文字 */
+.settings-hint {
+ display: block;
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--color-text-muted);
+}
+
+/* 设置面板预览框 */
+.preview-box {
+ padding: 12px;
+ background: var(--color-bg-tertiary);
+ border-radius: 4px;
+ font-size: 13px;
+ margin-bottom: 16px;
+}
diff --git a/frontend/src/components/TopBar/TopBar.jsx b/frontend/src/components/TopBar/TopBar.jsx
index b3c3183..74514db 100644
--- a/frontend/src/components/TopBar/TopBar.jsx
+++ b/frontend/src/components/TopBar/TopBar.jsx
@@ -29,6 +29,16 @@ const Toolbar = () => {
const { activeMap, fetchProfile } = useApiConfigStore();
const [coreModel, setCoreModel] = useState('未设置');
const [assistModel, setAssistModel] = useState('未设置');
+
+ // 系统设置状态
+ const [settings, setSettings] = useState({
+ thinkingTagPrefix: '
',
+ thinkingTagSuffix: '',
+ currentPresetName: null
+ });
+ const [settingsLoading, setSettingsLoading] = useState(false);
+ const [settingsMessage, setSettingsMessage] = useState({ type: '', text: '' });
+ const [settingsActiveTab, setSettingsActiveTab] = useState('display'); // 默认显示分页
// 点击外部关闭面板 - 使用 useCallback 优化
const handleClickOutside = React.useCallback((event) => {
@@ -43,6 +53,68 @@ const Toolbar = () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [handleClickOutside]);
+
+ // 加载系统设置
+ useEffect(() => {
+ if (activePanel === 'settings') {
+ loadSettings();
+ }
+ }, [activePanel]);
+
+ const loadSettings = async () => {
+ try {
+ setSettingsLoading(true);
+ const response = await fetch('/api/regex/settings');
+ const data = await response.json();
+
+ if (data.success) {
+ setSettings(data.settings);
+ }
+ } catch (error) {
+ console.error('加载系统设置失败:', error);
+ showSettingsMessage('error', '加载设置失败');
+ } finally {
+ setSettingsLoading(false);
+ }
+ };
+
+ const saveSettings = async () => {
+ try {
+ setSettingsLoading(true);
+ const response = await fetch('/api/regex/settings', {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(settings)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ showSettingsMessage('success', '设置已保存');
+ } else {
+ showSettingsMessage('error', '保存失败');
+ }
+ } catch (error) {
+ console.error('保存设置失败:', error);
+ showSettingsMessage('error', '保存失败');
+ } finally {
+ setSettingsLoading(false);
+ }
+ };
+
+ const showSettingsMessage = (type, text) => {
+ setSettingsMessage({ type, text });
+ setTimeout(() => setSettingsMessage({ type: '', text: '' }), 3000);
+ };
+
+ const handleSettingsInputChange = (field, value) => {
+ setSettings(prev => ({
+ ...prev,
+ [field]: value
+ }));
+ };
// ✅ 简化:直接调用 store 方法
const handleSidebarModeChange = (mode) => {
@@ -170,55 +242,231 @@ const Toolbar = () => {
{/* 设置面板 - 使用条件渲染优化 */}
{activePanel === 'settings' && (
-
+
系统设置
-
- {/* 左侧栏模式 */}
-
-
左侧栏模式
-
-
- handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法
- />
- 固定
-
-
- handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法
- />
- 智能
-
-
- handleSidebarModeChange(e.target.value)} // ✅ 使用 store 方法
- />
- 扩展
-
-
-
- {sidebarMode === 'fixed' && '左侧栏保持默认宽度,不响应交互'}
- {sidebarMode === 'smart' && '鼠标悬停时自动展开,移开后收起'}
- {sidebarMode === 'expanded' && '左侧栏保持最大宽度'}
-
+
+ {/* 消息提示 */}
+ {settingsMessage.text && (
+
+ {settingsMessage.text}
+ )}
+
+ {/* 分页标签 */}
+
+
+
+
+
+
+
+ {settingsLoading &&
加载中...
}
+
+ {/* 显示设置 */}
+ {settingsActiveTab === 'display' && (
+
+ )}
+
+ {/* 格式化设置 */}
+ {settingsActiveTab === 'formatting' && (
+
+
+
思考标签配置
+
+ 前缀
+ handleSettingsInputChange('thinkingTagPrefix', e.target.value)}
+ placeholder=""
+ className="settings-input"
+ />
+
+
+
+ 后缀
+ handleSettingsInputChange('thinkingTagSuffix', e.target.value)}
+ placeholder=""
+ className="settings-input"
+ />
+
+
+
+
+
原始内容:
+
+ 这是回复 {settings.thinkingTagPrefix}思考过程{settings.thinkingTagSuffix} 继续
+
+
+
+
+
+
+
+
正则规则管理
+
+
+ data/regex/
+
+
+ - 📁 global/ - 全局规则
+ - 📁 characters/ - 角色卡规则
+ - 📁 presets/ - 预设规则
+
+
+
+
+ 导入规则文件
+ {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await fetch('/api/regex/import', {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showSettingsMessage('success', data.message);
+ } else {
+ showSettingsMessage('error', '导入失败');
+ }
+ } catch (error) {
+ showSettingsMessage('error', '导入失败');
+ }
+ }}
+ className="settings-input"
+ />
+
+ 支持 SillyTavern 格式的规则文件
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* 用户设置 */}
+ {settingsActiveTab === 'user' && (
+
+
+
当前预设
+ handleSettingsInputChange('currentPresetName', e.target.value || null)}
+ placeholder="未选择"
+ className="settings-input"
+ />
+
+ 影响全局正则规则的启用
+
+
+
+
+
+
+
+ )}
diff --git a/frontend/src/components/TopBar/items/ThemeToggle/ThemeToggle.jsx b/frontend/src/components/TopBar/items/ThemeToggle/ThemeToggle.jsx
index 653827a..d81c6c5 100644
--- a/frontend/src/components/TopBar/items/ThemeToggle/ThemeToggle.jsx
+++ b/frontend/src/components/TopBar/items/ThemeToggle/ThemeToggle.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
+import useAppLayoutStore from '../../../../Store/AppLayoutSlice';
import './ThemeToggle.css';
const themes = [
@@ -50,19 +51,16 @@ const themes = [
const ThemeToggle = () => {
const [isOpen, setIsOpen] = useState(false);
- const [currentTheme, setCurrentTheme] = useState(() => {
- return localStorage.getItem('colorTheme') || 'dark';
- });
const panelRef = useRef(null);
+
+ // ✅ 从 Zustand store 获取主题状态和方法
+ const { colorTheme, setColorTheme } = useAppLayoutStore();
+ // ✅ 当 store 中的主题变化时,同步到 DOM
useEffect(() => {
- // 应用主题到 document
- document.documentElement.setAttribute('data-theme', currentTheme);
- document.documentElement.setAttribute('data-color-theme', currentTheme);
- // 保存到 localStorage
- localStorage.setItem('theme', currentTheme);
- localStorage.setItem('colorTheme', currentTheme);
- }, [currentTheme]);
+ document.documentElement.setAttribute('data-theme', colorTheme);
+ document.documentElement.setAttribute('data-color-theme', colorTheme);
+ }, [colorTheme]);
// 点击外部关闭面板
useEffect(() => {
@@ -82,11 +80,11 @@ const ThemeToggle = () => {
}, [isOpen]);
const handleThemeSelect = (themeId) => {
- setCurrentTheme(themeId);
+ setColorTheme(themeId); // ✅ 使用 store 的方法
setIsOpen(false);
};
- const currentThemeData = themes.find(t => t.id === currentTheme) || themes[2];
+ const currentThemeData = themes.find(t => t.id === colorTheme) || themes[2];
return (
@@ -108,7 +106,7 @@ const ThemeToggle = () => {
{themes.map(theme => (