diff --git a/video-gen-admin/dist/assets/index-CDdTfqKl.js b/video-gen-admin/dist/assets/index-CDdTfqKl.js new file mode 100644 index 00000000..ce628772 --- /dev/null +++ b/video-gen-admin/dist/assets/index-CDdTfqKl.js @@ -0,0 +1,405 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),u=o(((e,t)=>{t.exports=l()})),d=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=d()})),p=o((e=>{var t=f();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=u(),n=f(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),te=V(null),G=V(null);function ne(e,t){switch(U(te,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Wd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Wd(t),e=Gd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function K(){H(W),H(ee),H(te)}function re(e){e.memoizedState!==null&&U(G,e);var t=W.current,n=Gd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function ie(e){ee.current===e&&(H(W),H(ee)),G.current===e&&(H(G),tp._currentValue=R)}var ae,q;function oe(e){if(ae===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ae=t&&t[1]||``,q=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{se=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?oe(n):``}function le(e,t){switch(e.tag){case 26:case 27:case 5:return oe(e.type);case 16:return oe(`Lazy`);case 13:return e.child!==t&&t!==null?oe(`Suspense Fallback`):oe(`Suspense`);case 19:return oe(`SuspenseList`);case 0:case 15:return ce(e.type,!1);case 11:return ce(e.type.render,!1);case 1:return ce(e.type,!0);case 31:return oe(`Activity`);default:return``}}function ue(e){try{var t=``,n=null;do t+=le(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var de=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,me=t.unstable_shouldYield,he=t.unstable_requestPaint,ge=t.unstable_now,_e=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,ye=t.unstable_UserBlockingPriority,be=t.unstable_NormalPriority,xe=t.unstable_LowPriority,Se=t.unstable_IdlePriority,Ce=t.log,we=t.unstable_setDisableYieldValue,Te=null,Ee=null;function De(e){if(typeof Ce==`function`&&we(e),Ee&&typeof Ee.setStrictMode==`function`)try{Ee.setStrictMode(Te,e)}catch{}}var Oe=Math.clz32?Math.clz32:je,ke=Math.log,Ae=Math.LN2;function je(e){return e>>>=0,e===0?32:31-(ke(e)/Ae|0)|0}var Me=256,Ne=262144,Pe=4194304;function Fe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ie(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Fe(n))):i=Fe(o):i=Fe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Fe(n))):i=Fe(o)):i=Fe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Le(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Re(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ze(){var e=Pe;return Pe<<=1,!(Pe&62914560)&&(Pe=4194304),e}function Be(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ve(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function He(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),en=!1;if($t)try{var tn={};Object.defineProperty(tn,`passive`,{get:function(){en=!0}}),window.addEventListener(`test`,tn,tn),window.removeEventListener(`test`,tn,tn)}catch{en=!1}var nn=null,rn=null,an=null;function on(){if(an)return an;var e,t=rn,n=t.length,r,i=`value`in nn?nn.value:nn.textContent,a=i.length;for(e=0;e=Rn),Vn=` `,Hn=!1;function Un(e,t){switch(e){case`keyup`:return In.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Wn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Gn=!1;function Kn(e,t){switch(e){case`compositionend`:return Wn(t);case`keypress`:return t.which===32?(Hn=!0,Vn):null;case`textInput`:return e=t.data,e===Vn&&Hn?null:e;default:return null}}function qn(e,t){if(Gn)return e===`compositionend`||!Ln&&Un(e,t)?(e=on(),an=rn=nn=null,Gn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=hr(n)}}function _r(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?_r(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function vr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Dt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dt(e.document)}return t}function yr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var br=$t&&`documentMode`in document&&11>=document.documentMode,xr=null,Sr=null,Cr=null,wr=!1;function Tr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;wr||xr==null||xr!==Dt(r)||(r=xr,`selectionStart`in r&&yr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Cr&&mr(Cr,r)||(Cr=r,r=Od(Sr,`onSelect`),0>=o,i-=o,gi=1<<32-Oe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Ti&&vi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Ti&&vi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Ti&&vi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Ti&&vi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&va(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ta(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ri(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ni(o.type,o.key,o.props,null,e.mode,c),Ta(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=oi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=va(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,wa(o),c);if(o.$$typeof===C)return b(e,r,Y(e,o),c);Ea(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ii(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ca=0;var i=b(e,t,n,r);return Sa=null,i}catch(t){if(t===fa||t===ma)throw t;var a=Qr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Oa=Da(!0),ka=Da(!1),Aa=!1;function ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ma(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Na(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Pa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Pl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Yr(e),Jr(e,null,n),t}return Gr(e,r,t,n),Yr(e)}function Fa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}function Ia(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var La=!1;function Ra(){if(La){var e=ra;if(e!==null)throw e}}function za(e,t,n,r){La=!1;var i=e.updateQueue;Aa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Ll&f)===f:(r&f)===f){f!==0&&f===na&&(La=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Aa=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Ba(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Va(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,Os(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ds(e,t,oa(c,r),pu(e)):Ds(e,t,r,pu(e))}catch(n){Ds(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function _s(){}function vs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ys(e).queue;gs(e,a,t,R,n===null?_s:function(){return bs(e),n(r)})}function ys(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function bs(e){var t=ys(e);t.next===null&&(t=e.alternate.memoizedState),Ds(e,t.next.queue,{},pu())}function xs(){return Ki(tp)}function Ss(){return wo().memoizedState}function Cs(){return wo().memoizedState}function ws(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Na(n);var r=Pa(t,e,n);r!==null&&(hu(r,t,n),Fa(r,t,n)),t={cache:Qi()},e.payload=t;return}t=t.return}}function Ts(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ks(e)?As(t,n):(n=Kr(e,t,n,r),n!==null&&(hu(n,e,r),js(n,t,r)))}function Es(e,t,n){Ds(e,t,n,pu())}function Ds(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ks(e))As(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,pr(s,o))return Gr(e,t,i,0),Fl===null&&J(),!1}catch{}if(n=Kr(e,t,i,r),n!==null)return hu(n,e,r),js(n,t,r),!0}return!1}function Os(e,t,n,r){if(r={lane:2,revertLane:fd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},ks(e)){if(t)throw Error(i(479))}else t=Kr(e,n,r,2),t!==null&&hu(t,e,2)}function ks(e){var t=e.alternate;return e===ro||t!==null&&t===ro}function As(e,t){so=oo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function js(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}var Ms={readContext:Ki,use:Do,useCallback:mo,useContext:mo,useEffect:mo,useImperativeHandle:mo,useLayoutEffect:mo,useInsertionEffect:mo,useMemo:mo,useReducer:mo,useRef:mo,useState:mo,useDebugValue:mo,useDeferredValue:mo,useTransition:mo,useSyncExternalStore:mo,useId:mo,useHostTransitionStatus:mo,useFormState:mo,useActionState:mo,useOptimistic:mo,useMemoCache:mo,useCacheRefresh:mo};Ms.useEffectEvent=mo;var Ns={readContext:Ki,use:Do,useCallback:function(e,t){return Co().memoizedState=[e,t===void 0?null:t],e},useContext:Ki,useEffect:rs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ts(4194308,4,ls.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ts(4194308,4,e,t)},useInsertionEffect:function(e,t){ts(4,2,e,t)},useMemo:function(e,t){var n=Co();t=t===void 0?null:t;var r=e();if(co){De(!0);try{e()}finally{De(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Co();if(n!==void 0){var i=n(t);if(co){De(!0);try{n(t)}finally{De(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ts.bind(null,ro,e),[r.memoizedState,e]},useRef:function(e){var t=Co();return e={current:e},t.memoizedState=e},useState:function(e){e=zo(e);var t=e.queue,n=Es.bind(null,ro,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ds,useDeferredValue:function(e,t){return ms(Co(),e,t)},useTransition:function(){var e=zo(!1);return e=gs.bind(null,ro,e.queue,!0,!1),Co().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ro,a=Co();if(Ti){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Fl===null)throw Error(i(349));Ll&127||Po(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,rs(Io.bind(null,r,o,e),[e]),r.flags|=2048,$o(9,{destroy:void 0},Fo.bind(null,r,o,n,t),null),n},useId:function(){var e=Co(),t=Fl.identifierPrefix;if(Ti){var n=_i,r=gi;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=lo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[Ze]=t,o[Qe]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Ld(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Dc(t)}}return Mc(t),Oc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Dc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=te.current,Mi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ci,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Ze]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Pd(e.nodeValue,n)),e||ki(t,!0)}else e=Ud(e).createTextNode(r),e[Ze]=t,t.stateNode=e}return Mc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Mi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[Ze]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),e=!1}else n=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?($a(t),t):($a(t),null);if(t.flags&128)throw Error(i(558))}return Mc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Mi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[Ze]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),a=!1}else a=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?($a(t),t):($a(t),null)}return $a(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ac(t,t.updateQueue),Mc(t),null);case 4:return K(),e===null&&wd(t.stateNode.containerInfo),Mc(t),null;case 10:return Bi(t.type),Mc(t),null;case 19:if(H(eo),r=t.memoizedState,r===null)return Mc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)jc(r,!1);else{if(Wl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=to(e),o!==null){for(t.flags|=128,jc(r,!1),e=o.updateQueue,t.updateQueue=e,Ac(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ti(n,e),n=n.sibling;return U(eo,eo.current&1|2),Ti&&vi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ge()>tu&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304)}else{if(!a)if(e=to(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ac(t,e),jc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ti)return Mc(t),null}else 2*ge()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Mc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ge(),e.sibling=null,n=eo.current,U(eo,a?n&1|2:n&1),Ti&&vi(t,r.treeForkCount),e);case 22:case 23:return $a(t),Ka(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Mc(t),t.subtreeFlags&6&&(t.flags|=8192)):Mc(t),n=t.updateQueue,n!==null&&Ac(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(ca),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Bi(Zi),Mc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Pc(e,t){switch(xi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Bi(Zi),K(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ie(t),null;case 31:if(t.memoizedState!==null){if($a(t),t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if($a(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(eo),null;case 4:return K(),null;case 10:return Bi(t.type),null;case 22:case 23:return $a(t),Ka(),e!==null&&H(ca),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Bi(Zi),null;case 25:return null;default:return null}}function Fc(e,t){switch(xi(t),t.tag){case 3:Bi(Zi),K();break;case 26:case 27:case 5:ie(t);break;case 4:K();break;case 31:t.memoizedState!==null&&$a(t);break;case 13:$a(t);break;case 19:H(eo);break;case 10:Bi(t.type);break;case 22:case 23:$a(t),Ka(),e!==null&&H(ca);break;case 24:Bi(Zi)}}function Ic(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Gu(t,t.return,e)}}function Lc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Gu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Gu(t,t.return,e)}}function Rc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Va(t,n)}catch(t){Gu(e,e.return,t)}}}function zc(e,t,n){n.props=Bs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Gu(e,t,n)}}function Bc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Gu(e,t,n)}}function Vc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Gu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Gu(e,t,n)}else n.current=null}function Hc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Gu(e,e.return,t)}}function Uc(e,t,n){try{var r=e.stateNode;Rd(r,e.type,n,t),r[Qe]=t}catch(t){Gu(e,e.return,t)}}function Wc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&ef(e.type)||e.tag===4}function Gc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Wc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&ef(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Kc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Wt));else if(r!==4&&(r===27&&ef(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Kc(e,t,n),e=e.sibling;e!==null;)Kc(e,t,n),e=e.sibling}function qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&ef(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(qc(e,t,n),e=e.sibling;e!==null;)qc(e,t,n),e=e.sibling}function Jc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Ld(t,r,n),t[Ze]=e,t[Qe]=n}catch(t){Gu(e,e.return,t)}}var Yc=!1,Xc=!1,Zc=!1,Qc=typeof WeakSet==`function`?WeakSet:Set,$c=null;function el(e,t){if(e=e.containerInfo,Vd=up,e=vr(e),yr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Hd={focusedElem:e,selectionRange:n},up=!1,$c=t;$c!==null;)if(t=$c,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$c=e;else for(;$c!==null;){switch(t=$c,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Ld(o,r,n),o[Ze]=e,ut(o),r=o;break a;case`link`:var s=Wf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=gr(s,h),v=gr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,Pl&6)throw Error(i(331));var c=Pl;if(Pl|=4,kl(o.current),Sl(o,o.current,s,n),Pl=c,ad(0,!1),Ee&&typeof Ee.onPostCommitFiberRoot==`function`)try{Ee.onPostCommitFiberRoot(Te,o)}catch{}return!0}finally{L.p=a,I.T=r,Vu(e,t)}}function Wu(e,t,n){t=ci(n,t),t=Ks(e.stateNode,t,2),e=Pa(e,t,2),e!==null&&(Ve(e,2),id(e))}function Gu(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=ci(n,e),n=qs(2),r=Pa(t,n,2),r!==null&&(Js(n,r,t,e),Ve(r,2),id(r));break}}t=t.return}}function Ku(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Nl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Hl=!0,i.add(n),e=qu.bind(null,e,t,n),t.then(e,e))}function qu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Fl===e&&(Ll&n)===n&&(Wl===4||Wl===3&&(Ll&62914560)===Ll&&300>ge()-$l?!(Pl&2)&&Su(e,0):ql|=n,Yl===Ll&&(Yl=0)),id(e)}function Ju(e,t){t===0&&(t=ze()),e=qr(e,t),e!==null&&(Ve(e,t),id(e))}function Yu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ju(e,n)}function Xu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Ju(e,n)}function Zu(e,t){return fe(e,t)}var Qu=null,$u=null,ed=!1,td=!1,nd=!1,rd=0;function id(e){e!==$u&&e.next===null&&($u===null?Qu=$u=e:$u=$u.next=e),td=!0,ed||(ed=!0,dd())}function ad(e,t){if(!nd&&td){nd=!0;do for(var n=!1,r=Qu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Oe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ud(r,a))}else a=Ll,a=Ie(r,r===Fl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Le(r,a)||(n=!0,ud(r,a));r=r.next}while(n);nd=!1}}function od(){sd()}function sd(){td=ed=!1;var e=0;rd!==0&&Jd()&&(e=rd);for(var t=ge(),n=null,r=Qu;r!==null;){var i=r.next,a=cd(r,t);a===0?(r.next=null,n===null?Qu=i:n.next=i,i===null&&($u=n)):(n=r,(e!==0||a&3)&&(td=!0)),r=i}iu!==0&&iu!==5||ad(e,!1),rd!==0&&(rd=0)}function cd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&zd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function wf(e,t,n){var r=Cf;if(r&&typeof t==`string`&&t){var i=kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),vf.has(i)||(vf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Ld(t,`link`,e),ut(t),r.head.appendChild(t)))}}function Tf(e){bf.D(e),wf(`dns-prefetch`,e,null)}function Ef(e,t){bf.C(e,t),wf(`preconnect`,e,t)}function Df(e,t,n){bf.L(e,t,n);var r=Cf;if(r&&e&&t){var i=`link[rel="preload"][as="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=Nf(e);break;case`script`:a=Lf(e)}_f.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),_f.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Pf(a))||t===`script`&&r.querySelector(Rf(a))||(t=r.createElement(`link`),Ld(t,`link`,e),ut(t),r.head.appendChild(t)))}}function Of(e,t){bf.m(e,t);var n=Cf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+kt(r)+`"][href="`+kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Lf(e)}if(!_f.has(a)&&(e=h({rel:`modulepreload`,href:e},t),_f.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Rf(a)))return}r=n.createElement(`link`),Ld(r,`link`,e),ut(r),n.head.appendChild(r)}}}function kf(e,t,n){bf.S(e,t,n);var r=Cf;if(r&&e){var i=lt(r).hoistableStyles,a=Nf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Pf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=_f.get(a))&&Vf(e,n);var c=o=r.createElement(`link`);ut(c),Ld(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Bf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Af(e,t){bf.X(e,t);var n=Cf;if(n&&e){var r=lt(n).hoistableScripts,i=Lf(e),a=r.get(i);a||(a=n.querySelector(Rf(i)),a||(e=h({src:e,async:!0},t),(t=_f.get(i))&&Hf(e,t),a=n.createElement(`script`),ut(a),Ld(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function jf(e,t){bf.M(e,t);var n=Cf;if(n&&e){var r=lt(n).hoistableScripts,i=Lf(e),a=r.get(i);a||(a=n.querySelector(Rf(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=_f.get(i))&&Hf(e,t),a=n.createElement(`script`),ut(a),Ld(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t,n,r){var a=(a=te.current)?yf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Nf(n.href),n=lt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Nf(n.href);var o=lt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Pf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),_f.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},_f.set(e,n),o||If(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Lf(n),n=lt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Nf(e){return`href="`+kt(e)+`"`}function Pf(e){return`link[rel="stylesheet"][`+e+`]`}function Ff(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function If(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Ld(t,`link`,n),ut(t),e.head.appendChild(t))}function Lf(e){return`[src="`+kt(e)+`"]`}function Rf(e){return`script[async]`+e}function zf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+kt(n.href)+`"]`);if(r)return t.instance=r,ut(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ut(r),Ld(r,`style`,a),Bf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Nf(n.href);var o=e.querySelector(Pf(a));if(o)return t.state.loading|=4,t.instance=o,ut(o),o;r=Ff(n),(a=_f.get(a))&&Vf(r,a),o=(e.ownerDocument||e).createElement(`link`),ut(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Ld(o,`link`,r),t.state.loading|=4,Bf(o,n.precedence,e),t.instance=o;case`script`:return o=Lf(n.src),(a=e.querySelector(Rf(o)))?(t.instance=a,ut(a),a):(r=n,(a=_f.get(o))&&(r=h({},n),Hf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ut(a),Ld(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Bf(r,n.precedence,e));return t.instance}function Bf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Kf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function qf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Jf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Nf(r.href),a=t.querySelector(Pf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Zf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ut(a);return}a=t.ownerDocument||t,r=Ff(r),(i=_f.get(i))&&Vf(r,i),a=a.createElement(`link`),ut(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Ld(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Zf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Yf=0;function Xf(e,t){return e.stylesheets&&e.count===0&&$f(e,e.stylesheets),0Yf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Zf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)$f(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Qf=null;function $f(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Qf=new Map,t.forEach(ep,e),Qf=null,Zf.call(e))}function ep(e,t){if(!(t.state.loading&4)){var n=Qf.get(e);if(n)var r=n.get(null);else{n=new Map,Qf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()}))(),_=`modulepreload`,v=function(e){return`/`+e},y={},b=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=v(t,n),t in y)return;y[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:_,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},x=c(f(),1),S=`popstate`;function C(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function w(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return k(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:A(t)}return M(t,n,null,e)}function T(e,t){if(e===!1||e==null)throw Error(t)}function E(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function D(){return Math.random().toString(36).substring(2,10)}function O(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function k(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?j(t):t,state:n,key:t&&t.key||r||D(),mask:i}}function A({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function j(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function M(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u()+1;let d=O(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u();let i=O(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return N(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(S,d),c=e,()=>{i.removeEventListener(S,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function N(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),T(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:A(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function P(e,t,n=`/`){return F(e,t,n,!1)}function F(e,t,n,r,i){let a=oe((typeof t==`string`?j(t):t).pathname||`/`,n);if(a==null)return null;let o=i??L(e),s=null,c=q(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;T(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=he([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(T(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),R(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ne(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of z(e.path))a(e,t,!0,n)}),t}function z(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=z(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function B(e){e.sort((e,t)=>e.score===t.score?K(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var V=/^:[\w-]+$/,H=3,U=2,W=1,ee=10,te=-2,G=e=>e===`*`;function ne(e,t){let n=e.split(`/`),r=n.length;return n.some(G)&&(r+=te),t&&(r+=U),n.filter(e=>!G(e)).reduce((e,t)=>e+(V.test(t)?H:t===``?W:ee),r)}function K(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function re(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function ae(e,t=!1,n=!0){E(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function q(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return E(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function oe(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var se=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ce(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?j(e):e,a;return n?(n=me(n),a=n.startsWith(`/`)?le(n.substring(1),`/`):le(n,t)):a=t,{pathname:a,search:ve(r),hash:ye(i)}}function le(e,t){let n=ge(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function ue(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function de(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function fe(e){let t=de(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function pe(e,t,n,r=!1){let i;typeof e==`string`?i=j(e):(i={...e},T(!i.pathname||!i.pathname.includes(`?`),ue(`?`,`pathname`,`search`,i)),T(!i.pathname||!i.pathname.includes(`#`),ue(`#`,`pathname`,`hash`,i)),T(!i.search||!i.search.includes(`#`),ue(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=ce(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var me=e=>e.replace(/\/\/+/g,`/`),he=e=>me(e.join(`/`)),ge=e=>e.replace(/\/+$/,``),_e=e=>ge(e).replace(/^\/*/,`/`),ve=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,ye=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,be=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function xe(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Se(e){return he(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ce=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function we(e,t){let n=e;if(typeof n!=`string`||!se.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ce)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=oe(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{E(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Te=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Te);var Ee=[`GET`,...Te];new Set(Ee);var De=x.createContext(null);De.displayName=`DataRouter`;var Oe=x.createContext(null);Oe.displayName=`DataRouterState`;var ke=x.createContext(!1);function Ae(){return x.useContext(ke)}var je=x.createContext({isTransitioning:!1});je.displayName=`ViewTransition`;var Me=x.createContext(new Map);Me.displayName=`Fetchers`;var Ne=x.createContext(null);Ne.displayName=`Await`;var Pe=x.createContext(null);Pe.displayName=`Navigation`;var Fe=x.createContext(null);Fe.displayName=`Location`;var Ie=x.createContext({outlet:null,matches:[],isDataRoute:!1});Ie.displayName=`Route`;var Le=x.createContext(null);Le.displayName=`RouteError`;var Re=`REACT_ROUTER_ERROR`,ze=`REDIRECT`,Be=`ROUTE_ERROR_RESPONSE`;function Ve(e){if(e.startsWith(`${Re}:${ze}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function He(e){if(e.startsWith(`${Re}:${Be}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new be(t.status,t.statusText,t.data)}catch{}}function Ue(e,{relative:t}={}){T(We(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(Pe),{hash:i,pathname:a,search:o}=$e(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:he([n,a])),r.createHref({pathname:s,search:o,hash:i})}function We(){return x.useContext(Fe)!=null}function Ge(){return T(We(),`useLocation() may be used only in the context of a component.`),x.useContext(Fe).location}var Ke=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function qe(e){x.useContext(Pe).static||x.useLayoutEffect(e)}function Je(){let{isDataRoute:e}=x.useContext(Ie);return e?vt():Ye()}function Ye(){T(We(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(De),{basename:t,navigator:n}=x.useContext(Pe),{matches:r}=x.useContext(Ie),{pathname:i}=Ge(),a=JSON.stringify(fe(r)),o=x.useRef(!1);return qe(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(E(o.current,Ke),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=pe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:he([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Xe=x.createContext(null);function Ze(e){let t=x.useContext(Ie).outlet;return x.useMemo(()=>t&&x.createElement(Xe.Provider,{value:e},t),[t,e])}function Qe(){let{matches:e}=x.useContext(Ie);return e[e.length-1]?.params??{}}function $e(e,{relative:t}={}){let{matches:n}=x.useContext(Ie),{pathname:r}=Ge(),i=JSON.stringify(fe(n));return x.useMemo(()=>pe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function et(e,t){return tt(e,t)}function tt(e,t,n){T(We(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(Pe),{matches:i}=x.useContext(Ie),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;bt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=Ge(),d;if(t){let e=typeof t==`string`?j(t):t;T(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):P(e,{pathname:p});E(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),E(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=ct(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:he([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:he([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Fe.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function nt(){let e=_t(),t=xe(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var rt=x.createElement(nt,null),it=class extends x.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=He(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Ie.Provider,{value:this.props.routeContext},x.createElement(Le.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(ot,{error:e},t):t}};it.contextType=ke;var at=new WeakMap;function ot({children:e,error:t}){let{basename:n}=x.useContext(Pe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Ve(t.digest);if(e){let r=at.get(t);if(r)throw r;let i=we(e.location,n);if(Ce&&!at.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw at.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function st({routeContext:e,match:t,children:n}){let r=x.useContext(De);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Ie.Provider,{value:e},n)}function ct(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);T(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Se(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||rt,o&&(s<0&&c===0?(bt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(st,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(it,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function lt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ut(e){let t=x.useContext(De);return T(t,lt(e)),t}function dt(e){let t=x.useContext(Oe);return T(t,lt(e)),t}function ft(e){let t=x.useContext(Ie);return T(t,lt(e)),t}function pt(e){let t=ft(e),n=t.matches[t.matches.length-1];return T(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function mt(){return pt(`useRouteId`)}function ht(){return dt(`useNavigation`).navigation}function gt(){let{matches:e,loaderData:t}=dt(`useMatches`);return x.useMemo(()=>e.map(e=>I(e,t)),[e,t])}function _t(){let e=x.useContext(Le),t=dt(`useRouteError`),n=pt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function vt(){let{router:e}=ut(`useNavigate`),t=pt(`useNavigate`),n=x.useRef(!1);return qe(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{E(n.current,Ke),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var yt={};function bt(e,t,n){!t&&!yt[e]&&(yt[e]=!0,E(!1,n))}x.memo(xt);function xt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return tt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function St({to:e,replace:t,state:n,relative:r}){T(We(),` may be used only in the context of a component.`);let{static:i}=x.useContext(Pe);E(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=x.useContext(Ie),{pathname:o}=Ge(),s=Je(),c=pe(e,fe(a),o,r===`path`),l=JSON.stringify(c);return x.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function Ct(e){return Ze(e.context)}function wt(e){T(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Tt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){T(!We(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=j(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=oe(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return E(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:x.createElement(Pe.Provider,{value:c},x.createElement(Fe.Provider,{children:t,value:h}))}function Et({children:e,location:t}){return et(Dt(e),t)}x.Component;function Dt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,Dt(e.props.children,i));return}T(e.type===wt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),T(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Dt(e.props.children,i)),n.push(a)}),n}var Ot=`get`,kt=`application/x-www-form-urlencoded`;function At(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function jt(e){return At(e)&&e.tagName.toLowerCase()===`button`}function Mt(e){return At(e)&&e.tagName.toLowerCase()===`form`}function Nt(e){return At(e)&&e.tagName.toLowerCase()===`input`}function Pt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ft(e,t){return e.button===0&&(!t||t===`_self`)&&!Pt(e)}var It=null;function Lt(){if(It===null)try{new FormData(document.createElement(`form`),0),It=!1}catch{It=!0}return It}var Rt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function zt(e){return e!=null&&!Rt.has(e)?(E(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${kt}"`),null):e}function Bt(e,t){let n,r,i,a,o;if(Mt(e)){let o=e.getAttribute(`action`);r=o?oe(o,t):null,n=e.getAttribute(`method`)||Ot,i=zt(e.getAttribute(`enctype`))||kt,a=new FormData(e)}else if(jt(e)||Nt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + + + } + placeholder="关键词:标题/项目名/核心点" + style={{ width: 260 }} + value={inputKeyword} + onChange={e => setInputKeyword(e.target.value)} + onPressEnter={doSearch} + /> + setInputUserId(e.target.value)} + onPressEnter={doSearch} + /> + setInputUserName(e.target.value)} + onPressEnter={doSearch} + /> + + + + + + `共 ${value} 条`, + onChange: setPage, + }} + scroll={{ x: 1380 }} + columns={[ + { + title: '项目ID', + dataIndex: 'id', + width: 150, + render: (value: string) => {shortId(value)}, + }, + { + title: '用户', + width: 180, + render: (_, record) => ( + + {record.userName || '-'} + {shortId(record.userId)} + + ), + }, + { + title: '项目信息', + width: 260, + render: (_, record) => ( + + {record.title || record.targetProjectName || '-'} + 素材:{record.sourceProjectName || '-'} + 核心:{record.coreContentPoint || '-'} + + ), + }, + { title: '状态', dataIndex: 'status', width: 130, render: (v: string) => }, + { title: '当前步骤', dataIndex: 'currentStepCode', width: 140, render: (v: string) => STEP_MAP[v] || v || '-' }, + { title: '图片结果', dataIndex: 'finalImageUrl', width: 90, render: (v: string) => v ? : }, + { title: '视频结果', dataIndex: 'finalVideoUrl', width: 90, render: (v: string) => v ? : }, + { title: '错误信息', dataIndex: 'errorMessage', width: 220, ellipsis: true, render: (v: string) => v || '-' }, + { title: '创建时间', dataIndex: 'createdAt', width: 170, render: safeDate }, + { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: safeDate }, + { + title: '操作', + fixed: 'right', + width: 110, + render: (_, record) => ( + + ), + }, + ]} + /> + + + + ); +}; + +export default AdminHotOpeningReplications; diff --git a/video-gen-admin/src/pages/AdminImageEngines.tsx.tmp.30304.1779097765352 b/video-gen-admin/src/pages/AdminImageEngines.tsx.tmp.30304.1779097765352 deleted file mode 100644 index 0a1dad8b..00000000 --- a/video-gen-admin/src/pages/AdminImageEngines.tsx.tmp.30304.1779097765352 +++ /dev/null @@ -1,250 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, Card, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, -} from 'antd'; -import { - PictureOutlined, PlusOutlined, EditOutlined, DeleteOutlined, -} from '@ant-design/icons'; -import { getImageEngines, saveImageEngine, deleteImageEngine } from '../api'; - -interface ImageEngine { - id: string; - name: string; - provider: string; - apiBase: string; - apiKey: string; - modelName: string; - supportedSizes: string[]; - supportedStyles: string[]; - generateUrl: string; - isActive: boolean; - priority: number; -} - -function parseJsonArray(val: unknown): any[] { - if (Array.isArray(val)) return val; - if (typeof val === 'string') { - try { return JSON.parse(val); } catch { return []; } - } - return []; -} - -const AdminImageEngines: React.FC = () => { - const [engines, setEngines] = useState([]); - const [loading, setLoading] = useState(false); - const [modal, setModal] = useState<{ open: boolean; engine: ImageEngine | null }>({ open: false, engine: null }); - const [form] = Form.useForm(); - - const load = async () => { - setLoading(true); - try { - const data = await getImageEngines(); - setEngines(data.map((e: any) => ({ - ...e, - supportedSizes: parseJsonArray(e.supportedSizes), - supportedStyles: parseJsonArray(e.supportedStyles), - }))); - } catch { - message.error('加载图片引擎失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { load(); }, []); - - const handleSave = async () => { - try { - const values = await form.validateFields(); - const payload = { - name: values.name, - provider: values.provider, - api_base: values.apiBase, - api_key: values.apiKey, - model_name: values.modelName, - supported_sizes: JSON.stringify(values.supportedSizes || []), - supported_styles: JSON.stringify(values.supportedStyles || []), - generate_url: values.generateUrl || '', - is_active: values.isActive ?? true, - priority: values.priority ?? 0, - }; - if (modal.engine) { - await saveImageEngine({ id: modal.engine.id, ...payload }); - message.success('已更新'); - } else { - await saveImageEngine(payload); - message.success('已添加'); - } - setModal({ open: false, engine: null }); - form.resetFields(); - load(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '保存失败'); - } - }; - - const handleDelete = async (id: string) => { - try { - await deleteImageEngine(id); - message.success('已删除'); - load(); - } catch { - message.error('删除失败'); - } - }; - - const openEdit = (engine?: ImageEngine) => { - setModal({ open: true, engine: engine || null }); - if (engine) { - form.setFieldsValue(engine); - } else { - form.resetFields(); - form.setFieldsValue({ - isActive: true, priority: 0, - supportedSizes: ['1024x1024', '1024x1792', '1792x1024'], - supportedStyles: ['realistic', 'anime', 'oil_painting'], - }); - } - }; - - const columns = [ - { - title: '引擎名称', key: 'name', width: 180, - render: (_: any, r: ImageEngine) => ( -
-
-
- {r.name} -
{r.provider}
-
-
- ), - }, - { - title: '支持尺寸', dataIndex: 'supportedSizes', width: 280, - render: (sizes: string[]) => {sizes.map(s => {s})}, - }, - { - title: '支持风格', dataIndex: 'supportedStyles', width: 280, - render: (styles: string[]) => {styles.map(s => {s})}, - }, - { - title: '状态', dataIndex: 'isActive', width: 80, - render: (v: boolean) => {v ? '启用' : '停用'}, - }, - { - title: '操作', key: 'action', width: 150, fixed: 'right' as const, - render: (_: any, r: ImageEngine) => ( - - - handleDelete(r.id)}> - - - - ), - }, - ]; - - return ( -
- -
- - - 图片引擎配置 - {engines.length} 个引擎 - - -
- -
- - - {modal.engine ? '编辑引擎' : '添加引擎'}} - open={modal.open} - onOk={handleSave} - onCancel={() => { setModal({ open: false, engine: null }); form.resetFields(); }} - okText="保存" cancelText="取消" width={620} - > - -
- - - - - - - - - - - - - - - - - - -
- -
- - - {modal.engine ? '编辑引擎' : '添加引擎'}} - open={modal.open} - onOk={handleSave} - onCancel={() => { setModal({ open: false, engine: null }); form.resetFields(); }} - okText="保存" cancelText="取消" width={680} - > - -
- - - - - - - - - - - - - - - - - - -
- -
- - - {modal.item ? '编辑行业' : '添加行业'}} - open={modal.open} - onOk={handleSave} - onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }} - okText="保存" cancelText="取消" width={640} - confirmLoading={saving} - > - -
- - - - - - -
- - - - - - - - - - - {/* Option Groups */} -
- 行业选项配置 - - 添加选项组,每组包含名称和多个选项,前台将显示为下拉选择 - -
- - {(fields, { add, remove }) => ( -
- {fields.map(({ key, name, ...restField }) => ( -
-
- - - - - - -
- - -
- ); -}; - -export default AdminIndustries; diff --git a/video-gen-admin/src/pages/AdminIndustries.tsx.tmp.30304.1779097121934 b/video-gen-admin/src/pages/AdminIndustries.tsx.tmp.30304.1779097121934 deleted file mode 100644 index b9d0181e..00000000 --- a/video-gen-admin/src/pages/AdminIndustries.tsx.tmp.30304.1779097121934 +++ /dev/null @@ -1,490 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, Card, Form, Input, message, Modal, Popconfirm, Space, Switch, Table, Tag, Typography, Dropdown, Select, -} from 'antd'; -import { - AppstoreOutlined, PlusOutlined, EditOutlined, DeleteOutlined, DownOutlined, MinusCircleOutlined, - ShoppingCartOutlined, BookOutlined, HomeOutlined, FireOutlined, RocketOutlined, SkinOutlined, - CompassOutlined, HeartOutlined, CarOutlined, CameraOutlined, CloudOutlined, StarOutlined, - TrophyOutlined, ThunderboltOutlined, BulbOutlined, CoffeeOutlined, CrownOutlined, DashboardOutlined, - FlagOutlined, GlobalOutlined, GiftOutlined, LaptopOutlined, MobileOutlined, MonitorOutlined, - PayCircleOutlined, PictureOutlined, PlayCircleOutlined, SafetyOutlined, - ShoppingOutlined, SmileOutlined, SoundOutlined, TagOutlined, TeamOutlined, - ToolOutlined, TruckOutlined, VideoCameraOutlined, WalletOutlined, BankOutlined, - BuildOutlined, ExperimentOutlined, HighlightOutlined, IdcardOutlined, - MedicineBoxOutlined, ReadOutlined, RestOutlined, SketchOutlined, SolutionOutlined, -} from '@ant-design/icons'; -import { getIndustryConfigs, saveIndustryConfig, deleteIndustryConfig } from '../api'; - -interface OptionGroup { - name: string; - options: string[]; -} - -interface IndustryItem { - id: string; - key: string; - label: string; - icon: string; - description: string; - skills: string; - optionGroups: OptionGroup[]; - isActive: boolean; - sortOrder: number; -} - -// Icon library for industries -const ICON_OPTIONS: { name: string; label: string; component: React.ReactNode }[] = [ - { name: 'ShoppingCartOutlined', label: '购物', component: }, - { name: 'BookOutlined', label: '书籍', component: }, - { name: 'HomeOutlined', label: '房产', component: }, - { name: 'FireOutlined', label: '火', component: }, - { name: 'RocketOutlined', label: '科技', component: }, - { name: 'SkinOutlined', label: '时尚', component: }, - { name: 'CompassOutlined', label: '指南', component: }, - { name: 'HeartOutlined', label: '健康', component: }, - { name: 'CarOutlined', label: '汽车', component: }, - { name: 'CameraOutlined', label: '相机', component: }, - { name: 'CloudOutlined', label: '云', component: }, - { name: 'StarOutlined', label: '星', component: }, - { name: 'TrophyOutlined', label: '奖杯', component: }, - { name: 'ThunderboltOutlined', label: '闪电', component: }, - { name: 'BulbOutlined', label: '灯泡', component: }, - { name: 'CoffeeOutlined', label: '咖啡', component: }, - { name: 'CrownOutlined', label: '皇冠', component: }, - { name: 'DashboardOutlined', label: '仪表', component: }, - { name: 'FlagOutlined', label: '旗帜', component: }, - { name: 'GlobalOutlined', label: '全球', component: }, - { name: 'GiftOutlined', label: '礼物', component: }, - { name: 'LaptopOutlined', label: '笔记本', component: }, - { name: 'MobileOutlined', label: '手机', component: }, - { name: 'MonitorOutlined', label: '显示器', component: }, - { name: 'PayCircleOutlined', label: '支付', component: }, - { name: 'PictureOutlined', label: '图片', component: }, - { name: 'PlayCircleOutlined', label: '播放', component: }, - { name: 'SafetyOutlined', label: '安全', component: }, - { name: 'ShoppingOutlined', label: '商店', component: }, - { name: 'SmileOutlined', label: '笑脸', component: }, - { name: 'SoundOutlined', label: '声音', component: }, - { name: 'TagOutlined', label: '标签', component: }, - { name: 'TeamOutlined', label: '团队', component: }, - { name: 'ToolOutlined', label: '工具', component: }, - { name: 'TruckOutlined', label: '物流', component: }, - { name: 'VideoCameraOutlined', label: '视频', component: }, - { name: 'WalletOutlined', label: '钱包', component: }, - { name: 'BankOutlined', label: '银行', component: }, - { name: 'BuildOutlined', label: '建筑', component: }, - { name: 'ExperimentOutlined', label: '实验', component: }, - { name: 'HighlightOutlined', label: '高亮', component: }, - { name: 'IdcardOutlined', label: '名片', component: }, - { name: 'MedicineBoxOutlined', label: '医药', component: }, - { name: 'ReadOutlined', label: '阅读', component: }, - { name: 'RestOutlined', label: '休息', component: }, - { name: 'SketchOutlined', label: '钻石', component: }, - { name: 'SolutionOutlined', label: '方案', component: }, -]; - -const iconMap: Record = Object.fromEntries( - ICON_OPTIONS.map(o => [o.name, o.component]) -); - -function getIconComponent(name: string): React.ReactNode { - return iconMap[name] || ; -} - -// Parse skills JSON into { skillItems, optionGroups } -function parseSkills(raw: string | any[]): { skillItems: { key: string; label: string }[]; optionGroups: OptionGroup[] } { - try { - const arr = typeof raw === 'string' ? JSON.parse(raw) : raw; - if (!Array.isArray(arr)) return { skillItems: [], optionGroups: [] }; - const skillItems: { key: string; label: string }[] = []; - const optionGroups: OptionGroup[] = []; - for (const item of arr) { - if (item && item.type === 'option_group' && item.name && Array.isArray(item.options)) { - optionGroups.push({ name: item.name, options: item.options }); - } else if (item && item.key && item.label) { - skillItems.push({ key: item.key, label: item.label }); - } - } - return { skillItems, optionGroups }; - } catch { - return { skillItems: [], optionGroups: [] }; - } -} - -// Icon Picker Component -const IconPicker: React.FC<{ value?: string; onChange?: (v: string) => void }> = ({ value, onChange }) => { - const selected = ICON_OPTIONS.find(o => o.name === value); - return ( - ( -
-
- {ICON_OPTIONS.map(opt => ( -
{ e.preventDefault(); e.stopPropagation(); onChange?.(opt.name); }} title={opt.label} style={{ - width: 40, height: 40, borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', - cursor: 'pointer', fontSize: 18, transition: 'all 0.15s', - background: value === opt.name ? '#6366f1' : 'transparent', - color: value === opt.name ? '#fff' : '#64748b', - border: value === opt.name ? 'none' : '1px solid transparent', - }} - onMouseEnter={e => { if (value !== opt.name) e.currentTarget.style.background = '#f1f5f9'; }} - onMouseLeave={e => { if (value !== opt.name) e.currentTarget.style.background = 'transparent'; }} - > - {opt.component} -
- ))} -
-
- )} - > -
- {value ? ( - <> - {getIconComponent(value)} - {selected?.label || value} - - ) : ( - 选择图标 - )} - -
-
- ); -}; - -const AdminIndustries: React.FC = () => { - const [industries, setIndustries] = useState([]); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [modal, setModal] = useState<{ open: boolean; item: IndustryItem | null }>({ open: false, item: null }); - const [form] = Form.useForm(); - - const load = async () => { - setLoading(true); - try { - const data = await getIndustryConfigs(); - setIndustries(data.map((item: any) => { - const { skillItems, optionGroups } = parseSkills(item.skills); - return { - id: item.id, - key: item.key, - label: item.label, - icon: item.icon || '', - description: item.description || '', - skills: JSON.stringify(skillItems), - optionGroups, - isActive: item.is_active ?? item.isActive ?? true, - sortOrder: item.sort_order ?? item.sortOrder ?? 0, - }; - })); - } catch { - message.error('加载行业配置失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { load(); }, []); - - const handleSave = async () => { - try { - const values = await form.validateFields(); - setSaving(true); - - // Build skills array: skill items + option groups - const skillsArr: any[] = []; - if (values.skills_wentutujie?.trim()) { - skillsArr.push({ type: 'skill', key: '文图理解', label: values.skills_wentutujie.trim() }); - } - // Add option groups - const groups: OptionGroup[] = (values.optionGroups || []) - .filter((g: any) => g?.name?.trim()) - .map((g: any) => ({ - name: g.name.trim(), - options: (g.options || []).filter((o: string) => o?.trim()), - })) - .filter((g: OptionGroup) => g.options.length > 0); - for (const g of groups) { - skillsArr.push({ type: 'option_group', name: g.name, options: g.options }); - } - - const payload: any = { - key: values.key, - label: values.label, - icon: values.icon || '', - description: values.description || '', - skills: skillsArr, - is_active: values.is_active ?? true, - sort_order: values.sort_order ?? 0, - }; - if (modal.item?.id) { - await saveIndustryConfig({ id: modal.item.id, ...payload }); - message.success('已更新'); - } else { - await saveIndustryConfig(payload); - message.success('已添加'); - } - setModal({ open: false, item: null }); - form.resetFields(); - load(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '保存失败'); - } finally { - setSaving(false); - } - }; - - const handleDelete = async (id: string) => { - try { - await deleteIndustryConfig(id); - message.success('已删除'); - load(); - } catch (e: any) { - message.error(e?.message || '删除失败'); - } - }; - - const openEdit = (item?: IndustryItem) => { - setModal({ open: true, item: item || null }); - let wentutujie = ''; - let wentutujieImage = ''; - let optionGroups: OptionGroup[] = []; - if (item) { - // Parse skills for 文图理解 video and image prompts - try { - const arr = JSON.parse(item.skills); - if (Array.isArray(arr)) { - for (const s of arr) { - if (s.key === '文图理解生成视频提示词' || s.key === '文图理解') { wentutujie = s.label || ''; } - if (s.key === '文图理解生成图片提示词') { wentutujieImage = s.label || ''; } - } - } - } catch { /* ignore */ } - optionGroups = item.optionGroups.length > 0 ? item.optionGroups : [{ name: '', options: [] }]; - } - if (item) { - form.setFieldsValue({ - key: item.key, - label: item.label, - icon: item.icon || '', - description: item.description, - skills_wentutujie: wentutujie, - skills_wentutujie_image: wentutujieImage, - optionGroups, - is_active: item.isActive, - sort_order: item.sortOrder, - }); - } else { - form.resetFields(); - form.setFieldsValue({ is_active: true, sort_order: 0, optionGroups: [{ name: '', options: [] }] }); - } - }; - - const columns = [ - { - title: '行业', key: 'industry', width: 180, - render: (_: any, r: IndustryItem) => ( -
-
- {getIconComponent(r.icon)} -
-
- {r.label} -
{r.key}
-
-
- ), - }, - { title: '描述', dataIndex: 'description', ellipsis: true }, - { - title: '选项配置', key: 'optionGroups', width: 260, - render: (_: any, r: IndustryItem) => { - if (!r.optionGroups || r.optionGroups.length === 0) { - return 未配置; - } - return ( -
- {r.optionGroups.map((g, i) => ( -
- {g.name} - - {g.options.slice(0, 3).join('、')}{g.options.length > 3 ? `...${g.options.length}项` : ''} - -
- ))} -
- ); - }, - }, - { - title: '文图理解生成视频提示词', dataIndex: 'skills', ellipsis: true, - render: (v: string) => { - try { - const arr = JSON.parse(v); - if (Array.isArray(arr)) { - const wtj = arr.find((s: any) => s.key === '文图理解生成视频提示词' || s.key === '文图理解'); - if (wtj) return {wtj.label}; - } - } catch { /* ignore */ } - return -; - }, - }, - { - title: '文图理解生成图片提示词', dataIndex: 'skills', ellipsis: true, - render: (v: string) => { - try { - const arr = JSON.parse(v); - if (Array.isArray(arr)) { - const wtj = arr.find((s: any) => s.key === '文图理解生成图片提示词'); - if (wtj) return {wtj.label}; - } - } catch { /* ignore */ } - return -; - }, - }, - { - title: '状态', dataIndex: 'isActive', width: 80, - render: (v: boolean) => {v ? '启用' : '停用'}, - }, - { - title: '操作', key: 'action', width: 150, fixed: 'right' as const, - render: (_: any, r: IndustryItem) => ( - - - handleDelete(r.id)}> - - - - ), - }, - ]; - - return ( -
- -
- - - 行业与技能配置 - {industries.length} 个行业 - - -
- -
- - - {modal.item ? '编辑行业' : '添加行业'}} - open={modal.open} - onOk={handleSave} - onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }} - okText="保存" cancelText="取消" width={640} - confirmLoading={saving} - > -
-
- - - - - - -
- - - - - - - - - - - {/* Option Groups */} -
- 行业选项配置 - - 添加选项组,每组包含名称和多个选项,前台将显示为下拉选择 - -
- - {(fields, { add, remove }) => ( -
- {fields.map(({ key, name, ...restField }) => ( -
-
- - - - - - -
- - -
- ); -}; - -export default AdminIndustries; diff --git a/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098260634 b/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098260634 deleted file mode 100644 index 04e55c68..00000000 --- a/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098260634 +++ /dev/null @@ -1,318 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Layout, Menu, Avatar, Typography, Dropdown, Spin, Modal, Form, Input, Space, message } from 'antd'; -import { - DashboardOutlined, - UserOutlined, - RobotOutlined, - SettingOutlined, - BellOutlined, - ThunderboltOutlined, - LogoutOutlined, - LockOutlined, - WalletOutlined, - CalculatorOutlined, - DollarOutlined, - AppstoreOutlined, - PlayCircleOutlined, - GiftOutlined, - HomeOutlined, - StarOutlined, - HeartOutlined, - CameraOutlined, - FileTextOutlined, - HistoryOutlined, - VideoCameraOutlined, - PictureOutlined, -} from '@ant-design/icons'; -import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom'; -import { useAdminStore } from '../store'; -import { getMenuConfigs, adminChangePassword } from '../api'; - -const { Sider, Content } = Layout; - -const iconMap: Record = { - DashboardOutlined: , - UserOutlined: , - RobotOutlined: , - SettingOutlined: , - BellOutlined: , - LogoutOutlined: , - WalletOutlined: , - CalculatorOutlined: , - DollarOutlined: , - AppstoreOutlined: , - PlayCircleOutlined: , - GiftOutlined: , - HomeOutlined: , - StarOutlined: , - HeartOutlined: , - CameraOutlined: , - FileTextOutlined: , - HistoryOutlined: , - VideoCameraOutlined: , -}; - -const AdminLayout: React.FC = () => { - const navigate = useNavigate(); - const location = useLocation(); - const { user, loading, logout } = useAdminStore(); - const [collapsed, setCollapsed] = useState(false); - const [menuItems, setMenuItems] = useState([]); - const [pwdModal, setPwdModal] = useState(false); - const [pwdForm] = Form.useForm(); - - useEffect(() => { - getMenuConfigs().then(data => { - let menus = data.filter((m: any) => { - const target = m.menu_target ?? m.menuTarget ?? 'admin'; - const active = m.is_active ?? m.isActive ?? true; - return active && (target === 'admin' || target === 'both'); - }); - // Non-admin backend users: filter by allowedMenus - const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); - if (user && !isAdminUser) { - const allowed = user.allowedMenus ?? (user as any)?.allowed_menus; - if (allowed && Array.isArray(allowed) && allowed.length > 0) { - const allowedSet = new Set(allowed); - menus = menus.filter((m: any) => { - const menuType = m.menu_type ?? m.menuType; - if (menuType === 'group') { - return menus.some((c: any) => { - const pid = c.parent_id ?? c.parentId; - return pid === m.id && allowedSet.has(c.path); - }); - } - return allowedSet.has(m.path); - }); - } else { - // No allowed menus set and not admin = show nothing - menus = []; - } - } - setMenuItems(menus); - }).catch(() => {}); - }, [user]); - - if (loading) { - return ( -
- -
- ); - } - - if (!user) { - return ; - } - - // Build menu items for Ant Design Menu — single sorted list interleaving groups and pages - const childMap: Record = {}; - menuItems.filter(m => (m.menu_type ?? m.menuType) !== 'group' && (m.parent_id ?? m.parentId)).forEach(m => { - const pid = m.parent_id ?? m.parentId; - if (!childMap[pid]) childMap[pid] = []; - childMap[pid].push(m); - }); - - // Sort all menu items by sortOrder, then build ant menu items - const sorted = [...menuItems].sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)); - const antMenuItems: any[] = []; - - sorted.forEach(m => { - const menuType = m.menu_type ?? m.menuType; - const parentId = m.parent_id ?? m.parentId; - if (menuType === 'group') { - const children = (childMap[m.id] || []) - .sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)) - .map(c => ({ - key: c.path, - icon: iconMap[c.icon] || undefined, - label: c.label, - })); - if (children.length > 0) { - antMenuItems.push({ - key: `group-${m.id}`, - icon: iconMap[m.icon] || undefined, - label: m.label, - children, - }); - } - } else if (!parentId) { - antMenuItems.push({ - key: m.path, - icon: iconMap[m.icon] || undefined, - label: m.label, - }); - } - }); - - // Fallback only for admin users — non-admin users with no permissions see nothing - const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); - if (antMenuItems.length === 0 && isAdminUser) { - antMenuItems.push( - { key: '/', icon: , label: '数据概览' }, - { key: '/users', icon: , label: '用户管理' }, - ); - } - - const selectedKey = location.pathname; - - // Find the leaf menu key that matches (for sub-menus, need to find the right key) - let activeKey = selectedKey; - const allLeafKeys: string[] = []; - antMenuItems.forEach(item => { - if (item.children) { - item.children.forEach((c: any) => allLeafKeys.push(c.key)); - } else { - allLeafKeys.push(item.key); - } - }); - // Exact match or prefix match - if (!allLeafKeys.includes(activeKey)) { - activeKey = allLeafKeys.find(k => activeKey.startsWith(k)) || '/'; - } - - // Find open keys for sub-menus - const openKeys: string[] = []; - antMenuItems.forEach(item => { - if (item.children) { - if (item.children.some((c: any) => c.key === activeKey)) { - openKeys.push(item.key); - } - } - }); - - return ( - - - {/* Logo */} -
-
- -
- {!collapsed && ( - - 管理后台 - - )} -
- - {/* Menu */} - { - if (key.startsWith('group-')) return; - navigate(key); - }} - style={{ background: 'transparent', borderRight: 0, marginTop: 8 }} - theme="dark" - /> - - - - - {/* Header */} -
- - {antMenuItems.find(m => m.key === activeKey)?.label - || antMenuItems.flatMap(m => m.children || []).find((c: any) => c.key === activeKey)?.label - || '管理后台'} - - , label: user?.username, disabled: true }, - { type: 'divider' as const }, - { key: 'changePwd', icon: , label: '修改密码' }, - { key: 'logout', icon: , label: '退出登录', danger: true }, - ], - onClick: ({ key }) => { - if (key === 'logout') { logout(); navigate('/login'); } - if (key === 'changePwd') { setPwdModal(true); pwdForm.resetFields(); } - }, - }} placement="bottomRight" arrow> -
- } - style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }} /> - - {user?.username} - -
-
-
- - {/* Content */} - - {antMenuItems.length === 0 && !isAdminUser ? ( -
- -
暂无任何权限
-
请联系管理员配置菜单权限
-
- ) : ( - - )} -
-
- - 修改密码} - open={pwdModal} - onOk={async () => { - try { - const values = await pwdForm.validateFields(); - if (values.newPassword !== values.confirmPassword) { - message.error('两次输入的密码不一致'); - return; - } - await adminChangePassword(values.oldPassword, values.newPassword); - message.success('密码修改成功'); - setPwdModal(false); - pwdForm.resetFields(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '修改失败'); - } - }} - onCancel={() => { setPwdModal(false); pwdForm.resetFields(); }} - okText="确认修改" cancelText="取消" width={420} - > -
- - - - - - - - - - -
- - ); -}; - -export default AdminLayout; diff --git a/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098268240 b/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098268240 deleted file mode 100644 index b22f3cd1..00000000 --- a/video-gen-admin/src/pages/AdminLayout.tsx.tmp.30304.1779098268240 +++ /dev/null @@ -1,319 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Layout, Menu, Avatar, Typography, Dropdown, Spin, Modal, Form, Input, Space, message } from 'antd'; -import { - DashboardOutlined, - UserOutlined, - RobotOutlined, - SettingOutlined, - BellOutlined, - ThunderboltOutlined, - LogoutOutlined, - LockOutlined, - WalletOutlined, - CalculatorOutlined, - DollarOutlined, - AppstoreOutlined, - PlayCircleOutlined, - GiftOutlined, - HomeOutlined, - StarOutlined, - HeartOutlined, - CameraOutlined, - FileTextOutlined, - HistoryOutlined, - VideoCameraOutlined, - PictureOutlined, -} from '@ant-design/icons'; -import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom'; -import { useAdminStore } from '../store'; -import { getMenuConfigs, adminChangePassword } from '../api'; - -const { Sider, Content } = Layout; - -const iconMap: Record = { - DashboardOutlined: , - UserOutlined: , - RobotOutlined: , - SettingOutlined: , - BellOutlined: , - LogoutOutlined: , - WalletOutlined: , - CalculatorOutlined: , - DollarOutlined: , - AppstoreOutlined: , - PlayCircleOutlined: , - GiftOutlined: , - HomeOutlined: , - StarOutlined: , - HeartOutlined: , - CameraOutlined: , - FileTextOutlined: , - HistoryOutlined: , - VideoCameraOutlined: , - PictureOutlined: , -}; - -const AdminLayout: React.FC = () => { - const navigate = useNavigate(); - const location = useLocation(); - const { user, loading, logout } = useAdminStore(); - const [collapsed, setCollapsed] = useState(false); - const [menuItems, setMenuItems] = useState([]); - const [pwdModal, setPwdModal] = useState(false); - const [pwdForm] = Form.useForm(); - - useEffect(() => { - getMenuConfigs().then(data => { - let menus = data.filter((m: any) => { - const target = m.menu_target ?? m.menuTarget ?? 'admin'; - const active = m.is_active ?? m.isActive ?? true; - return active && (target === 'admin' || target === 'both'); - }); - // Non-admin backend users: filter by allowedMenus - const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); - if (user && !isAdminUser) { - const allowed = user.allowedMenus ?? (user as any)?.allowed_menus; - if (allowed && Array.isArray(allowed) && allowed.length > 0) { - const allowedSet = new Set(allowed); - menus = menus.filter((m: any) => { - const menuType = m.menu_type ?? m.menuType; - if (menuType === 'group') { - return menus.some((c: any) => { - const pid = c.parent_id ?? c.parentId; - return pid === m.id && allowedSet.has(c.path); - }); - } - return allowedSet.has(m.path); - }); - } else { - // No allowed menus set and not admin = show nothing - menus = []; - } - } - setMenuItems(menus); - }).catch(() => {}); - }, [user]); - - if (loading) { - return ( -
- -
- ); - } - - if (!user) { - return ; - } - - // Build menu items for Ant Design Menu — single sorted list interleaving groups and pages - const childMap: Record = {}; - menuItems.filter(m => (m.menu_type ?? m.menuType) !== 'group' && (m.parent_id ?? m.parentId)).forEach(m => { - const pid = m.parent_id ?? m.parentId; - if (!childMap[pid]) childMap[pid] = []; - childMap[pid].push(m); - }); - - // Sort all menu items by sortOrder, then build ant menu items - const sorted = [...menuItems].sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)); - const antMenuItems: any[] = []; - - sorted.forEach(m => { - const menuType = m.menu_type ?? m.menuType; - const parentId = m.parent_id ?? m.parentId; - if (menuType === 'group') { - const children = (childMap[m.id] || []) - .sort((a, b) => (a.sort_order ?? a.sortOrder ?? 0) - (b.sort_order ?? b.sortOrder ?? 0)) - .map(c => ({ - key: c.path, - icon: iconMap[c.icon] || undefined, - label: c.label, - })); - if (children.length > 0) { - antMenuItems.push({ - key: `group-${m.id}`, - icon: iconMap[m.icon] || undefined, - label: m.label, - children, - }); - } - } else if (!parentId) { - antMenuItems.push({ - key: m.path, - icon: iconMap[m.icon] || undefined, - label: m.label, - }); - } - }); - - // Fallback only for admin users — non-admin users with no permissions see nothing - const isAdminUser = !!(user?.isAdmin ?? (user as any)?.is_admin); - if (antMenuItems.length === 0 && isAdminUser) { - antMenuItems.push( - { key: '/', icon: , label: '数据概览' }, - { key: '/users', icon: , label: '用户管理' }, - ); - } - - const selectedKey = location.pathname; - - // Find the leaf menu key that matches (for sub-menus, need to find the right key) - let activeKey = selectedKey; - const allLeafKeys: string[] = []; - antMenuItems.forEach(item => { - if (item.children) { - item.children.forEach((c: any) => allLeafKeys.push(c.key)); - } else { - allLeafKeys.push(item.key); - } - }); - // Exact match or prefix match - if (!allLeafKeys.includes(activeKey)) { - activeKey = allLeafKeys.find(k => activeKey.startsWith(k)) || '/'; - } - - // Find open keys for sub-menus - const openKeys: string[] = []; - antMenuItems.forEach(item => { - if (item.children) { - if (item.children.some((c: any) => c.key === activeKey)) { - openKeys.push(item.key); - } - } - }); - - return ( - - - {/* Logo */} -
-
- -
- {!collapsed && ( - - 管理后台 - - )} -
- - {/* Menu */} - { - if (key.startsWith('group-')) return; - navigate(key); - }} - style={{ background: 'transparent', borderRight: 0, marginTop: 8 }} - theme="dark" - /> - - - - - {/* Header */} -
- - {antMenuItems.find(m => m.key === activeKey)?.label - || antMenuItems.flatMap(m => m.children || []).find((c: any) => c.key === activeKey)?.label - || '管理后台'} - - , label: user?.username, disabled: true }, - { type: 'divider' as const }, - { key: 'changePwd', icon: , label: '修改密码' }, - { key: 'logout', icon: , label: '退出登录', danger: true }, - ], - onClick: ({ key }) => { - if (key === 'logout') { logout(); navigate('/login'); } - if (key === 'changePwd') { setPwdModal(true); pwdForm.resetFields(); } - }, - }} placement="bottomRight" arrow> -
- } - style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }} /> - - {user?.username} - -
-
-
- - {/* Content */} - - {antMenuItems.length === 0 && !isAdminUser ? ( -
- -
暂无任何权限
-
请联系管理员配置菜单权限
-
- ) : ( - - )} -
-
- - 修改密码} - open={pwdModal} - onOk={async () => { - try { - const values = await pwdForm.validateFields(); - if (values.newPassword !== values.confirmPassword) { - message.error('两次输入的密码不一致'); - return; - } - await adminChangePassword(values.oldPassword, values.newPassword); - message.success('密码修改成功'); - setPwdModal(false); - pwdForm.resetFields(); - } catch (e: any) { - if (e?.errorFields) return; - message.error(e?.message || '修改失败'); - } - }} - onCancel={() => { setPwdModal(false); pwdForm.resetFields(); }} - okText="确认修改" cancelText="取消" width={420} - > -
- - - - - - - - - - -
- - ); -}; - -export default AdminLayout; diff --git a/video-gen-admin/src/pages/AdminReplicationProjectDetail.tsx b/video-gen-admin/src/pages/AdminReplicationProjectDetail.tsx new file mode 100644 index 00000000..9627d07d --- /dev/null +++ b/video-gen-admin/src/pages/AdminReplicationProjectDetail.tsx @@ -0,0 +1,384 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + Alert, + Button, + Card, + Collapse, + Descriptions, + Empty, + Space, + Spin, + Steps, + Table, + Tag, + Tooltip, + Typography, + message, +} from 'antd'; +import { + ArrowLeftOutlined, + FileImageOutlined, + PlayCircleOutlined, + VideoCameraOutlined, +} from '@ant-design/icons'; +import { useNavigate, useParams } from 'react-router-dom'; +import { getAdminHotOpeningTaskDetail, getAdminShotProjectDetail } from '../api'; +import type { ReplicationProjectDetailOut, ReplicationStepOut } from '../types'; +import { formatDate } from '../utils/formatDate'; +import JsonCollapse, { JsonBlock } from './adminReplication/components/JsonCollapse'; +import MediaPreview from './adminReplication/components/MediaPreview'; +import StatusTag, { getModuleLabel, getStatusLabel, getStepCodeLabel } from './adminReplication/components/StatusTag'; +import VideoPromptSchemaViewer from './adminReplication/components/VideoPromptSchemaViewer'; + +type ReplicationModuleType = 'hot_opening_replicate' | 'shot_replicate'; + +interface AdminReplicationProjectDetailProps { + moduleType?: ReplicationModuleType; +} + +const STEP_ORDER = [ + 'material_input', + 'image_prompt_optimize', + 'image_generate', + 'video_prompt_optimize', + 'video_generate', +]; + +const STEP_DESCRIPTIONS: Record = { + material_input: '参考素材、项目名称和核心内容点', + image_prompt_optimize: '图片 AI 提词优化结果', + image_generate: '图片引擎、生成参数和结果图', + video_prompt_optimize: '视频 JSON schema、动作流程和最终提词', + video_generate: '视频引擎、生成参数、封面和结果视频', +}; + +const safeDate = (value?: string | null): string => (value ? formatDate(value) : '-'); + +const shortId = (value?: string | null): string => { + if (!value) return '-'; + return value.length > 18 ? `${value.slice(0, 10)}...${value.slice(-4)}` : value; +}; + +const EmptyText: React.FC<{ text?: string }> = ({ text = '-' }) => ( + {text} +); + +const StepRawJson: React.FC<{ step?: ReplicationStepOut | null }> = ({ step }) => { + if (!step) return null; + return ; +}; + +const StepHeader: React.FC<{ index: number; stepCode: string; step?: ReplicationStepOut | null; current?: boolean }> = ({ index, stepCode, step, current }) => ( + + {`第${index}步:${getStepCodeLabel(stepCode)}`} + + {current ? 当前步骤 : null} + {STEP_DESCRIPTIONS[stepCode] || stepCode} + {step?.updatedAt ? 更新时间:{safeDate(step.updatedAt)} : null} + {step?.chatTaskId ? Chat:{shortId(step.chatTaskId)} : null} + +); + +const buildDefaultActiveKeys = (detail: ReplicationProjectDetailOut | null, stepsByCode: Record): string[] => { + if (!detail) return []; + const keys = new Set(); + keys.add('material_input'); + if (detail.currentStepCode) keys.add(detail.currentStepCode); + Object.values(stepsByCode).forEach(step => { + if (step.status === 'failed' || step.errorMessage) keys.add(step.stepCode); + }); + if (detail.errorMessage) keys.add(detail.currentStepCode || 'material_input'); + return Array.from(keys); +}; + +const renderErrorAlert = (messageText?: string | null, title = '错误信息') => { + if (!messageText) return null; + return ; +}; + +const renderPromptText = (value?: string | null, empty = '暂无提词') => { + if (!value) return ; + return {value}; +}; + +const AdminReplicationProjectDetail: React.FC = ({ moduleType = 'shot_replicate' }) => { + const { projectId } = useParams<{ projectId: string }>(); + const navigate = useNavigate(); + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(false); + + const load = useCallback(async () => { + if (!projectId) return; + setLoading(true); + try { + const res = moduleType === 'hot_opening_replicate' + ? await getAdminHotOpeningTaskDetail(projectId) + : await getAdminShotProjectDetail(projectId); + setDetail(res); + } catch (e: any) { + message.error(e?.message || '加载复刻项目详情失败'); + } finally { + setLoading(false); + } + }, [moduleType, projectId]); + + useEffect(() => { + load(); + }, [load]); + + const stepsByCode = useMemo(() => { + const map: Record = {}; + (detail?.steps || []).forEach(step => { map[step.stepCode] = step; }); + return map; + }, [detail]); + + const moduleValue = detail?.module || moduleType; + const moduleName = getModuleLabel(moduleValue); + + const stepItems = useMemo(() => STEP_ORDER.map(code => { + const step = stepsByCode[code]; + let status: 'wait' | 'process' | 'finish' | 'error' = 'wait'; + if (step?.status === 'completed') status = 'finish'; + else if (step?.status === 'processing') status = 'process'; + else if (step?.status === 'failed') status = 'error'; + return { + title: getStepCodeLabel(code), + description: step ? : '未创建', + status, + }; + }), [stepsByCode]); + + const defaultActiveKeys = useMemo(() => buildDefaultActiveKeys(detail, stepsByCode), [detail, stepsByCode]); + + if (loading && !detail) { + return
; + } + + if (!detail) { + return ( + + + + ); + } + + const materialStep = stepsByCode.material_input; + const imagePromptStep = stepsByCode.image_prompt_optimize; + const imageGenerateStep = stepsByCode.image_generate; + const videoPromptStep = stepsByCode.video_prompt_optimize; + const videoGenerateStep = stepsByCode.video_generate; + + const imageResultUrl = detail.imageGeneration?.resultImageUrl || detail.finalImageUrl; + const videoCoverUrl = detail.videoGeneration?.resultVideoCoverUrl || detail.finalVideoCoverUrl; + const videoResultUrl = detail.videoGeneration?.resultVideoUrl || detail.finalVideoUrl; + + const collapseItems = [ + { + key: 'material_input', + label: , + children: ( + + + + {detail.material?.materialStepId || '-'} + {detail.material?.sourceProjectName || '-'} + {detail.material?.targetProjectName || '-'} + {detail.material?.coreContentPoint || '-'} + +
+ 素材视频}> + + + 素材图片}> + + +
+ {renderErrorAlert(materialStep?.errorMessage)} + +
+ ), + }, + { + key: 'image_prompt_optimize', + label: , + children: ( + + + {detail.imageGeneration?.promptStepId || '-'} + + {renderPromptText(detail.imageGeneration?.prompt, '暂无图片提示词')} + + {renderErrorAlert(imagePromptStep?.errorMessage || detail.imageGeneration?.errorMessage)} + + + ), + }, + { + key: 'image_generate', + label: , + children: ( + + + {detail.imageGeneration?.generateStepId || '-'} + {detail.imageGeneration?.chatTaskId || '-'} + + {detail.imageGeneration?.engineId || '-'} + {detail.imageGeneration?.engineName || '-'} + + + {renderErrorAlert(detail.imageGeneration?.errorMessage || imageGenerateStep?.errorMessage)} + 图片结果}> + + + + + ), + }, + { + key: 'video_prompt_optimize', + label: , + children: ( + + + {detail.videoGeneration?.promptStepId || '-'} + + + + {renderErrorAlert(videoPromptStep?.errorMessage || detail.videoGeneration?.errorMessage)} + + + + ), + }, + { + key: 'video_generate', + label: , + children: ( + + + {detail.videoGeneration?.generateStepId || '-'} + {detail.videoGeneration?.chatTaskId || '-'} + + {detail.videoGeneration?.engineId || '-'} + {detail.videoGeneration?.engineName || '-'} + + + {renderErrorAlert(detail.videoGeneration?.errorMessage || videoGenerateStep?.errorMessage)} +
+ 视频封面}> + + + 最终视频}> + + +
+ +
+ ), + }, + ]; + + return ( +
+ + + + + {moduleName}项目详情 + + {detail.module && detail.module !== moduleType ? 接口模块:{getModuleLabel(detail.module)} : null} + + + + + + + {detail.id} + + {detail.userId || '-'} + {detail.userName || '-'} + {detail.title || '-'} + {getStepCodeLabel(detail.currentStepCode)} + + {safeDate(detail.createdAt)} + {safeDate(detail.updatedAt)} + {safeDate(detail.completedAt)} + {detail.errorMessage ? : null} + + + + + + + + +
+ + + + + + + + + +
+
+ + + + {getStepCodeLabel(v)} }, + { title: '状态', dataIndex: 'status', width: 120, render: (v: string) => }, + { title: '版本', dataIndex: 'version', width: 70 }, + { title: '当前有效', dataIndex: 'isCurrent', width: 90, render: (v: boolean) => v ? : }, + { title: 'Chat任务', dataIndex: 'chatTaskId', width: 160, render: (v: string) => {shortId(v)} }, + { title: '创建时间', dataIndex: 'createdAt', width: 170, render: safeDate }, + { title: '完成时间', dataIndex: 'completedAt', width: 170, render: safeDate }, + { title: '错误', dataIndex: 'errorMessage', ellipsis: true, render: (v: string) => v || '-' }, + ]} + /> + ), + }, + { + key: 'raw-detail', + label: '完整详情原始 JSON', + children: , + }, + ]} + /> +
+
+ ); +}; + +export default AdminReplicationProjectDetail; diff --git a/video-gen-admin/src/pages/AdminShotReplications.tsx b/video-gen-admin/src/pages/AdminShotReplications.tsx new file mode 100644 index 00000000..74c8e2e6 --- /dev/null +++ b/video-gen-admin/src/pages/AdminShotReplications.tsx @@ -0,0 +1,220 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Card, DatePicker, Input, Progress, Select, Space, Table, Tag, Tooltip, Typography, message } from 'antd'; +import { EyeOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { getAdminShotTaskSets } from '../api'; +import type { ShotTaskSetOut } from '../types'; +import { formatDate } from '../utils/formatDate'; + +const PAGE_SIZE = 20; + +const TASK_STATUS_OPTIONS = [ + { value: 'pending_analysis', label: '等待分析' }, + { value: 'analyzing', label: '分析中' }, + { value: 'analysis_completed', label: '分析完成' }, + { value: 'analysis_failed', label: '分析失败' }, + { value: 'splitting', label: '拆镜中' }, + { value: 'split_completed', label: '拆镜完成' }, + { value: 'partial_failed', label: '部分失败' }, + { value: 'failed', label: '失败' }, +]; + +const ANALYSIS_STATUS_OPTIONS = [ + { value: 'pending', label: '待分析' }, + { value: 'processing', label: '分析中' }, + { value: 'completed', label: '分析完成' }, + { value: 'failed', label: '分析失败' }, +]; + +const SPLIT_STATUS_OPTIONS = [ + { value: 'none', label: '未拆镜' }, + { value: 'pending', label: '待拆镜' }, + { value: 'processing', label: '拆镜中' }, + { value: 'completed', label: '拆镜完成' }, + { value: 'failed', label: '拆镜失败' }, + { value: 'retry_waiting', label: '等待重试' }, +]; + +const STATUS_MAP: Record = { + pending_analysis: { color: 'default', text: '等待分析' }, + analyzing: { color: 'processing', text: '分析中' }, + analysis_completed: { color: 'success', text: '分析完成' }, + analysis_failed: { color: 'error', text: '分析失败' }, + splitting: { color: 'warning', text: '拆镜中' }, + split_completed: { color: 'success', text: '拆镜完成' }, + partial_failed: { color: 'orange', text: '部分失败' }, + failed: { color: 'error', text: '失败' }, + none: { color: 'default', text: '未拆镜' }, + pending: { color: 'default', text: '待处理' }, + processing: { color: 'warning', text: '处理中' }, + completed: { color: 'success', text: '完成' }, + retry_waiting: { color: 'orange', text: '等待重试' }, +}; + +const safeDate = (value?: string | null): string => (value ? formatDate(value) : '-'); +const shortId = (value?: string | null): string => (!value ? '-' : value.length > 16 ? `${value.slice(0, 10)}...` : value); + +const StatusTag: React.FC<{ status?: string | null }> = ({ status }) => { + if (!status) return -; + const meta = STATUS_MAP[status] || { color: 'blue', text: status }; + return {meta.text}; +}; + +const AdminShotReplications: React.FC = () => { + const navigate = useNavigate(); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + + const [status, setStatus] = useState(''); + const [analysisStatus, setAnalysisStatus] = useState(''); + const [splitStatus, setSplitStatus] = useState(''); + const [inputKeyword, setInputKeyword] = useState(''); + const [inputUserId, setInputUserId] = useState(''); + const [inputUserName, setInputUserName] = useState(''); + const [createdRange, setCreatedRange] = useState(null); + + const [queryKeyword, setQueryKeyword] = useState(''); + const [queryUserId, setQueryUserId] = useState(''); + const [queryUserName, setQueryUserName] = useState(''); + const [queryCreatedRange, setQueryCreatedRange] = useState(null); + const [reloadKey, setReloadKey] = useState(0); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await getAdminShotTaskSets({ + status: status || undefined, + analysisStatus: analysisStatus || undefined, + splitStatus: splitStatus || undefined, + keyword: queryKeyword || undefined, + userId: queryUserId || undefined, + userName: queryUserName || undefined, + createdStart: queryCreatedRange?.[0]?.toISOString?.(), + createdEnd: queryCreatedRange?.[1]?.toISOString?.(), + page, + pageSize: PAGE_SIZE, + }); + setItems(res.items || []); + setTotal(res.total || 0); + } catch (e: any) { + message.error(e?.message || '加载拆镜复刻任务失败'); + } finally { + setLoading(false); + } + }, [analysisStatus, page, queryCreatedRange, queryKeyword, queryUserId, queryUserName, splitStatus, status]); + + useEffect(() => { + load(); + }, [load, reloadKey]); + + const doSearch = () => { + setQueryKeyword(inputKeyword.trim()); + setQueryUserId(inputUserId.trim()); + setQueryUserName(inputUserName.trim()); + setQueryCreatedRange(createdRange); + setPage(1); + }; + + const resetSearch = () => { + setStatus(''); + setAnalysisStatus(''); + setSplitStatus(''); + setInputKeyword(''); + setInputUserId(''); + setInputUserName(''); + setCreatedRange(null); + setQueryKeyword(''); + setQueryUserId(''); + setQueryUserName(''); + setQueryCreatedRange(null); + setPage(1); + setReloadKey(v => v + 1); + }; + + return ( +
+ + + +
+ 拆镜复刻 + 只读排查页面:查看拆镜总任务、AI 分析结果、切片列表和关联复刻项目。 +
+ +
+ + + { setAnalysisStatus(v || ''); setPage(1); }} options={ANALYSIS_STATUS_OPTIONS} /> + } placeholder="关键词:标题/内容/分类/受众" style={{ width: 260 }} value={inputKeyword} onChange={e => setInputKeyword(e.target.value)} onPressEnter={doSearch} /> + setInputUserId(e.target.value)} onPressEnter={doSearch} /> + setInputUserName(e.target.value)} onPressEnter={doSearch} /> + + + + + +
`共 ${value} 条`, onChange: setPage }} + scroll={{ x: 1500 }} + columns={[ + { title: '任务集ID', dataIndex: 'id', width: 150, render: (v: string) => {shortId(v)} }, + { + title: '用户', + width: 180, + render: (_, record) => ( + + {record.userName || '-'} + {shortId(record.userId)} + + ), + }, + { + title: '任务信息', + width: 300, + render: (_, record) => ( + + {record.title || '-'} + 分类:{record.originalVideoCategory || '-'} + 受众:{record.originalVideoAudience || '-'} + + ), + }, + { title: '总状态', dataIndex: 'status', width: 120, render: (v: string) => }, + { title: '分析状态', dataIndex: 'analysisStatus', width: 110, render: (v: string) => }, + { title: '拆镜状态', dataIndex: 'splitStatus', width: 110, render: (v: string) => }, + { + title: '切片进度', + width: 180, + render: (_, record) => { + const totalSegments = record.segmentCount || 0; + const done = record.completedSegmentCount || 0; + const percent = totalSegments ? Math.round((done / totalSegments) * 100) : 0; + return `${done}/${totalSegments}`} />; + }, + }, + { title: '失败片段', dataIndex: 'failedSegmentCount', width: 90, render: (v: number) => v ? {v} : 0 }, + { title: '视频时长', dataIndex: 'videoDurationSeconds', width: 100, render: (v: number) => `${Number(v || 0).toFixed(1)}s` }, + { title: '创建时间', dataIndex: 'createdAt', width: 170, render: safeDate }, + { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: safeDate }, + { + title: '操作', + fixed: 'right', + width: 120, + render: (_, record) => , + }, + ]} + /> + + + + ); +}; + +export default AdminShotReplications; diff --git a/video-gen-admin/src/pages/AdminShotTaskSetDetail.tsx b/video-gen-admin/src/pages/AdminShotTaskSetDetail.tsx new file mode 100644 index 00000000..8abf82d8 --- /dev/null +++ b/video-gen-admin/src/pages/AdminShotTaskSetDetail.tsx @@ -0,0 +1,340 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + Alert, + Button, + Card, + Collapse, + Descriptions, + Drawer, + Empty, + Input, + Select, + Space, + Spin, + Table, + Tag, + Tooltip, + Typography, + message, +} from 'antd'; +import { ArrowLeftOutlined, EyeOutlined, PlayCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; +import { useNavigate, useParams } from 'react-router-dom'; +import { getAdminShotSegmentDetail, getAdminShotSegments, getAdminShotTaskSetDetail } from '../api'; +import type { ShotAiSuggestionOut, ShotSegmentDetailOut, ShotSegmentOut, ShotTaskSetDetailOut } from '../types'; +import { formatDate } from '../utils/formatDate'; +import { getStepCodeLabel } from './adminReplication/components/StatusTag'; + +const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; +const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, ''); +const PAGE_SIZE = 20; + +const SOURCE_OPTIONS = [ + { value: 'ai_suggestion', label: 'AI 建议' }, + { value: 'custom', label: '自定义' }, +]; +const SPLIT_STATUS_OPTIONS = [ + { value: 'none', label: '未拆镜' }, + { value: 'pending', label: '待拆镜' }, + { value: 'processing', label: '拆镜中' }, + { value: 'completed', label: '拆镜完成' }, + { value: 'failed', label: '拆镜失败' }, + { value: 'retry_waiting', label: '等待重试' }, +]; +const ANALYSIS_STATUS_OPTIONS = [ + { value: 'not_required', label: '无需分析' }, + { value: 'pending', label: '待分析' }, + { value: 'processing', label: '分析中' }, + { value: 'completed', label: '分析完成' }, + { value: 'failed', label: '分析失败' }, +]; +const REPLICATE_STATUS_OPTIONS = [ + { value: 'not_started', label: '未复刻' }, + { value: 'project_created', label: '已创建项目' }, + { value: 'processing', label: '复刻中' }, + { value: 'completed', label: '复刻完成' }, + { value: 'failed', label: '复刻失败' }, +]; + +const STATUS_MAP: Record = { + pending_analysis: { color: 'default', text: '等待分析' }, + analyzing: { color: 'processing', text: '分析中' }, + analysis_completed: { color: 'success', text: '分析完成' }, + analysis_failed: { color: 'error', text: '分析失败' }, + splitting: { color: 'warning', text: '拆镜中' }, + split_completed: { color: 'success', text: '拆镜完成' }, + partial_failed: { color: 'orange', text: '部分失败' }, + failed: { color: 'error', text: '失败' }, + none: { color: 'default', text: '未拆镜' }, + pending: { color: 'default', text: '待处理' }, + processing: { color: 'warning', text: '处理中' }, + completed: { color: 'success', text: '完成' }, + retry_waiting: { color: 'orange', text: '等待重试' }, + not_required: { color: 'default', text: '无需分析' }, + not_started: { color: 'default', text: '未复刻' }, + project_created: { color: 'processing', text: '已创建项目' }, +}; + +const apiUrl = (url?: string | null): string => { + if (!url) return ''; + const value = String(url).trim(); + if (!value) return ''; + if (/^(https?:)?\/\//i.test(value) || /^(blob|data):/i.test(value)) return value; + return `${RESOURCE_BASE}${value.startsWith('/') ? value : `/${value}`}`; +}; + +const safeDate = (value?: string | null): string => (value ? formatDate(value) : '-'); +const shortId = (value?: string | null): string => (!value ? '-' : value.length > 16 ? `${value.slice(0, 10)}...` : value); + +const StatusTag: React.FC<{ status?: string | null }> = ({ status }) => { + if (!status) return -; + const meta = STATUS_MAP[status] || { color: 'blue', text: status }; + return {meta.text}; +}; + +const JsonBlock: React.FC<{ value: unknown; maxHeight?: number }> = ({ value, maxHeight = 420 }) => ( +
+    {JSON.stringify(value ?? {}, null, 2)}
+  
+); + +const VideoPreview: React.FC<{ url?: string | null; height?: number }> = ({ url, height = 280 }) => { + if (!url) return ; + return
`${Number(v || 0).toFixed(2)}s` }, + { title: '结束', dataIndex: 'endSecond', width: 80, render: (v: number) => `${Number(v || 0).toFixed(2)}s` }, + { title: '内容', dataIndex: 'content', ellipsis: true }, + { title: '分类', dataIndex: 'category', width: 140 }, + { title: '受众', dataIndex: 'audience', ellipsis: true }, + ]} + /> +); + +const AdminShotTaskSetDetail: React.FC = () => { + const { taskSetId } = useParams<{ taskSetId: string }>(); + const navigate = useNavigate(); + + const [detail, setDetail] = useState(null); + const [segments, setSegments] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [segmentLoading, setSegmentLoading] = useState(false); + const [reloadKey, setReloadKey] = useState(0); + + const [sourceMode, setSourceMode] = useState(''); + const [splitStatus, setSplitStatus] = useState(''); + const [analysisStatus, setAnalysisStatus] = useState(''); + const [replicateStatus, setReplicateStatus] = useState(''); + const [drawerOpen, setDrawerOpen] = useState(false); + const [segmentDetail, setSegmentDetail] = useState(null); + + const loadDetail = useCallback(async () => { + if (!taskSetId) return; + setLoading(true); + try { + const res = await getAdminShotTaskSetDetail(taskSetId); + setDetail(res); + } catch (e: any) { + message.error(e?.message || '加载拆镜任务详情失败'); + } finally { + setLoading(false); + } + }, [taskSetId]); + + const loadSegments = useCallback(async () => { + if (!taskSetId) return; + setSegmentLoading(true); + try { + const res = await getAdminShotSegments(taskSetId, { + sourceMode: sourceMode || undefined, + splitStatus: splitStatus || undefined, + analysisStatus: analysisStatus || undefined, + replicateStatus: replicateStatus || undefined, + page, + pageSize: PAGE_SIZE, + }); + setSegments(res.items || []); + setTotal(res.total || 0); + } catch (e: any) { + message.error(e?.message || '加载拆镜片段失败'); + } finally { + setSegmentLoading(false); + } + }, [analysisStatus, page, replicateStatus, sourceMode, splitStatus, taskSetId]); + + useEffect(() => { loadDetail(); }, [loadDetail, reloadKey]); + useEffect(() => { loadSegments(); }, [loadSegments, reloadKey]); + + const openSegmentDetail = async (segmentId: string) => { + setDrawerOpen(true); + setSegmentDetail(null); + try { + const res = await getAdminShotSegmentDetail(segmentId); + setSegmentDetail(res); + } catch (e: any) { + message.error(e?.message || '加载片段详情失败'); + } + }; + + const resetFilters = () => { + setSourceMode(''); + setSplitStatus(''); + setAnalysisStatus(''); + setReplicateStatus(''); + setPage(1); + setReloadKey(v => v + 1); + }; + + if (loading && !detail) { + return
; + } + + if (!detail) { + return ; + } + + return ( +
+ + + + + 拆镜任务详情 + + + + + + + + {detail.id} + {detail.userId || '-'} + {detail.userName || '-'} + {detail.title || '-'} + {Number(detail.videoDurationSeconds || 0).toFixed(2)}s + + + + {detail.completedSegmentCount}/{detail.segmentCount},失败 {detail.failedSegmentCount} + {detail.originalVideoCategory || '-'} + {safeDate(detail.createdAt)} + {safeDate(detail.updatedAt)} + {detail.originalVideoContent || '-'} + {detail.originalVideoAudience || '-'} + {detail.analysisErrorMessage ? : null} + {detail.splitErrorMessage ? : null} + + + + 原视频预览}> + + + + + {detail.aiSuggestions?.length ? : } + }]} + /> + + + + + { setSplitStatus(v || ''); setPage(1); }} /> + { setReplicateStatus(v || ''); setPage(1); }} /> + + + + +
`共 ${value} 条`, onChange: setPage }} + scroll={{ x: 1550 }} + columns={[ + { title: '片段ID', dataIndex: 'id', width: 150, render: (v: string) => {shortId(v)} }, + { title: '序号', dataIndex: 'segmentIndex', width: 70 }, + { title: '来源', dataIndex: 'sourceMode', width: 100, render: (v: string) => v === 'ai_suggestion' ? AI建议 : 自定义 }, + { title: '时间节点', dataIndex: 'timeNode', width: 130 }, + { title: '时长', dataIndex: 'durationSeconds', width: 90, render: (v: number) => `${Number(v || 0).toFixed(2)}s` }, + { title: '切割', dataIndex: 'splitStatus', width: 100, render: (v: string) => }, + { title: '分析', dataIndex: 'analysisStatus', width: 100, render: (v: string) => }, + { title: '复刻', dataIndex: 'replicateStatus', width: 110, render: (v: string) => }, + { title: '片段内容', dataIndex: 'segmentContent', width: 260, ellipsis: true, render: (v: string) => v || '-' }, + { title: '分类', dataIndex: 'segmentCategory', width: 120, render: (v: string) => v || '-' }, + { + title: '关联项目', + width: 220, + render: (_, record) => record.moduleProjectId ? ( + + + {record.moduleProjectTitle || getStepCodeLabel(record.moduleProjectCurrentStepCode)} + + + ) : 未创建, + }, + { title: '错误', dataIndex: 'splitLastError', width: 220, ellipsis: true, render: (v: string, record) => v || record.analysisErrorMessage || '-' }, + { title: '创建时间', dataIndex: 'createdAt', width: 170, render: safeDate }, + { + title: '操作', + fixed: 'right', + width: 100, + render: (_, record) => , + }, + ]} + /> + + + + setDrawerOpen(false)} destroyOnClose> + {!segmentDetail ? : ( + + + {segmentDetail.id} + {segmentDetail.timeNode} + {Number(segmentDetail.durationSeconds || 0).toFixed(2)}s + + + + {segmentDetail.moduleProjectId ? : '-'} + {segmentDetail.segmentContent || '-'} + {segmentDetail.segmentCategory || '-'} + {segmentDetail.segmentAudience || '-'} + + + + + {segmentDetail.splitLastError ? : null} + {segmentDetail.analysisErrorMessage ? : null} + }, + { key: 'suggestion', label: 'AI 建议原始 JSON', children: }, + ]} + /> + + )} + + + ); +}; + +export default AdminShotTaskSetDetail; diff --git a/video-gen-admin/src/pages/adminReplication/components/JsonCollapse.tsx b/video-gen-admin/src/pages/adminReplication/components/JsonCollapse.tsx new file mode 100644 index 00000000..18f139fa --- /dev/null +++ b/video-gen-admin/src/pages/adminReplication/components/JsonCollapse.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Collapse, Empty } from 'antd'; + +interface JsonBlockProps { + value: unknown; + maxHeight?: number; +} + +export const JsonBlock: React.FC = ({ value, maxHeight = 420 }) => { + if (value === undefined || value === null || value === '') { + return ; + } + + return ( +
+      {JSON.stringify(value, null, 2)}
+    
+ ); +}; + +interface JsonCollapseProps { + input?: unknown; + output?: unknown; + raw?: unknown; + inputLabel?: string; + outputLabel?: string; + rawLabel?: string; +} + +const JsonCollapse: React.FC = ({ + input, + output, + raw, + inputLabel = '查看 input_json', + outputLabel = '查看 output_json', + rawLabel = '查看原始 JSON', +}) => { + const items = []; + if (input !== undefined) items.push({ key: 'input', label: inputLabel, children: }); + if (output !== undefined) items.push({ key: 'output', label: outputLabel, children: }); + if (raw !== undefined) items.push({ key: 'raw', label: rawLabel, children: }); + + if (!items.length) return null; + return ; +}; + +export default JsonCollapse; diff --git a/video-gen-admin/src/pages/adminReplication/components/MediaPreview.tsx b/video-gen-admin/src/pages/adminReplication/components/MediaPreview.tsx new file mode 100644 index 00000000..8a9b9b3e --- /dev/null +++ b/video-gen-admin/src/pages/adminReplication/components/MediaPreview.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Alert, Button, Empty, Image, Space, Typography, message } from 'antd'; +import { CopyOutlined, LinkOutlined } from '@ant-design/icons'; + +const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; +const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, ''); + +export function resolveResourceUrl(url?: string | null): string { + if (!url) return ''; + const value = String(url).trim(); + if (!value) return ''; + if (/^(https?:)?\/\//i.test(value) || /^(blob|data):/i.test(value)) return value; + return `${RESOURCE_BASE}${value.startsWith('/') ? value : `/${value}`}`; +} + +interface MediaPreviewProps { + url?: string | null; + type: 'image' | 'video'; + height?: number; + emptyDescription?: string; + errorDescription?: string; +} + +const MediaPreview: React.FC = ({ + url, + type, + height = 260, + emptyDescription = '暂无资源', + errorDescription = '资源加载失败,可能文件不存在、签名过期或访问权限受限。', +}) => { + const resolvedUrl = useMemo(() => resolveResourceUrl(url), [url]); + const [loadFailed, setLoadFailed] = useState(false); + + useEffect(() => { + setLoadFailed(false); + }, [resolvedUrl]); + + const copyUrl = async () => { + if (!resolvedUrl) return; + try { + await navigator.clipboard.writeText(resolvedUrl); + message.success('资源地址已复制'); + } catch { + message.error('复制失败,请手动复制'); + } + }; + + const tools = resolvedUrl ? ( + + + + + {resolvedUrl} + + + ) : null; + + if (!resolvedUrl) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ {loadFailed ? ( + + ) : null} + + {type === 'image' ? ( + setLoadFailed(true)} + style={{ + width: '100%', + maxHeight: height, + objectFit: 'contain', + borderRadius: 12, + background: '#f8fafc', + border: '1px solid #f1f5f9', + }} + fallback="" + preview={!loadFailed} + /> + ) : ( +
+ ); +}; + +export default MediaPreview; diff --git a/video-gen-admin/src/pages/adminReplication/components/StatusTag.tsx b/video-gen-admin/src/pages/adminReplication/components/StatusTag.tsx new file mode 100644 index 00000000..207347e0 --- /dev/null +++ b/video-gen-admin/src/pages/adminReplication/components/StatusTag.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Tag, Tooltip } from 'antd'; + +interface LabelMeta { + text: string; + color?: string; +} + +const STATUS_LABELS: Record = { + // 通用任务状态 + pending: { text: '待处理', color: 'default' }, + waiting_user: { text: '等待用户操作', color: 'processing' }, + processing: { text: '处理中', color: 'warning' }, + completed: { text: '已完成', color: 'success' }, + failed: { text: '失败', color: 'error' }, + cancelled: { text: '已取消', color: 'default' }, + canceled: { text: '已取消', color: 'default' }, + timeout: { text: '已超时', color: 'error' }, + queued: { text: '已入队', color: 'processing' }, + preparing: { text: '准备中', color: 'processing' }, + + // 生成任务 pipeline / download stage + creating_provider_task: { text: '创建远端任务', color: 'processing' }, + waiting_remote: { text: '等待远端结果', color: 'processing' }, + polling: { text: '轮询远端结果', color: 'processing' }, + result_ready: { text: '结果已就绪', color: 'success' }, + downloading: { text: '下载中', color: 'processing' }, + done: { text: '已完成', color: 'success' }, + download_failed: { text: '下载失败', color: 'error' }, + retry_waiting: { text: '等待重试', color: 'orange' }, + + // 拆镜总任务状态 + pending_analysis: { text: '等待分析', color: 'default' }, + analyzing: { text: '分析中', color: 'processing' }, + analysis_completed: { text: '分析完成', color: 'success' }, + analysis_failed: { text: '分析失败', color: 'error' }, + splitting: { text: '拆镜中', color: 'warning' }, + split_completed: { text: '拆镜完成', color: 'success' }, + partial_failed: { text: '部分失败', color: 'orange' }, + + // 拆镜片段状态 + none: { text: '未拆镜', color: 'default' }, + not_required: { text: '无需处理', color: 'default' }, + not_started: { text: '未开始', color: 'default' }, + project_created: { text: '已创建项目', color: 'processing' }, + + // 布尔/兜底类 + true: { text: '是', color: 'success' }, + false: { text: '否', color: 'default' }, +}; + +const STEP_LABELS: Record = { + material_input: '素材输入', + image_prompt_optimize: '图片 AI 提词', + image_generate: '图片生成', + video_prompt_optimize: '视频 AI 提词', + video_generate: '视频生成', +}; + +const MODULE_LABELS: Record = { + hot_opening_replicate: '爆款开头复刻', + shot_replicate: '拆镜复刻', +}; + +const SOURCE_MODE_LABELS: Record = { + ai_suggestion: { text: 'AI 建议', color: 'purple' }, + custom: { text: '自定义', color: 'cyan' }, + original: { text: '原视频', color: 'blue' }, +}; + +export function getModuleLabel(value?: string | null): string { + if (!value) return '-'; + return MODULE_LABELS[value] || value; +} + +export function getStepCodeLabel(value?: string | null): string { + if (!value) return '-'; + return STEP_LABELS[value] || value; +} + +export function getStatusLabel(value?: string | null): string { + if (!value) return '-'; + return STATUS_LABELS[value]?.text || STEP_LABELS[value] || MODULE_LABELS[value] || SOURCE_MODE_LABELS[value]?.text || value; +} + +export function getStatusColor(value?: string | null): string { + if (!value) return 'default'; + return STATUS_LABELS[value]?.color || SOURCE_MODE_LABELS[value]?.color || 'blue'; +} + +interface StatusTagProps { + status?: string | boolean | null; + tooltipRaw?: boolean; +} + +const StatusTag: React.FC = ({ status, tooltipRaw = true }) => { + if (status === undefined || status === null || status === '') return -; + const raw = String(status); + const label = getStatusLabel(raw); + const tag = {label}; + if (!tooltipRaw || label === raw) return tag; + return {tag}; +}; + +export default StatusTag; diff --git a/video-gen-admin/src/pages/adminReplication/components/VideoPromptSchemaViewer.tsx b/video-gen-admin/src/pages/adminReplication/components/VideoPromptSchemaViewer.tsx new file mode 100644 index 00000000..dc70e791 --- /dev/null +++ b/video-gen-admin/src/pages/adminReplication/components/VideoPromptSchemaViewer.tsx @@ -0,0 +1,314 @@ +import React from 'react'; +import { Card, Collapse, Descriptions, Empty, Space, Table, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { JsonBlock } from './JsonCollapse'; + +const FIELD_LABELS: Record = { + schema_version: 'Schema 版本', + generation_type: '生成类型', + business_type: '业务类型', + usage: '用途', + product_type: '产品类型', + target_audience: '目标受众', + style: '风格', + aspect_ratio: '画面比例', + resolution: '分辨率', + duration: '时长', + fps: '帧率', + final_prompt: '最终提示词', + prompt: '提示词', + description: '说明', + content: '内容', + time: '时间', + time_range: '时间段', + start_time: '开始时间', + end_time: '结束时间', + stage: '阶段', + scene: '场景', + action: '动作', + camera: '镜头', + shot: '镜头', + lens: '镜头', + subtitle: '字幕', + voiceover: '口播', + audio: '音频', + rhythm: '节奏', + notes: '备注', +}; + +const SECTION_LABELS: Record = { + basic: '基础信息', + basic_info: '基础信息', + material: '素材理解', + material_understanding: '素材理解', + business: '业务属性', + business_info: '业务属性', + visual: '画面与风格', + visual_style: '画面与风格', + time_plan: '时间规划', + timeline: '时间规划', + scene_timeline: '场景时间线', + action_flow: '动作流程', + motion_flow: '动作流程', + character_action_flow: '角色动作流程', + camera_flow: '镜头流程', + shot_flow: '镜头流程', + lens_flow: '镜头流程', + subtitle: '字幕', + subtitles: '字幕', + voiceover: '口播', + audio: '音频', + rhythm: '节奏', + compliance: '合规控制', + final_prompt: '最终提示词', +}; + +const isRecord = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); + +const labelOf = (key: string): string => SECTION_LABELS[key] || FIELD_LABELS[key] || key; + +const isLongText = (value: unknown): boolean => typeof value === 'string' && value.length > 80; + + +const EMPTY_TEXTS = new Set(['', '无', 'null', 'None', 'none', '未提及', '不适用']); +const PLACEHOLDER_FLOW_TEXTS = new Set([ + '展示主体动作、核心卖点或主要视觉内容', + '展示主要动作、核心卖点或主要视觉内容', + '展示核心卖点或主要视觉内容', + '展示主体动作', + '无', +]); +const ACTION_CONTENT_KEYS = ['动作内容', '动作', '动作说明', '内容', '说明', '主体动作', '动作变化']; +const CAMERA_CONTENT_KEYS = ['镜头内容', '镜头', '镜头说明', '运镜', '运镜说明', '内容', '说明']; + +const toText = (value: unknown): string => { + if (value === null || value === undefined) return ''; + if (typeof value === 'string') return value.trim(); + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +const isEmptyText = (value: unknown): boolean => EMPTY_TEXTS.has(toText(value)); + +const isActionFlowSection = (sectionKey: string): boolean => { + const lower = sectionKey.toLowerCase(); + return lower.includes('action') || lower.includes('motion') || lower.includes('动作'); +}; + +const isCameraFlowSection = (sectionKey: string): boolean => { + const lower = sectionKey.toLowerCase(); + return lower.includes('camera') || lower.includes('shot') || lower.includes('lens') || lower.includes('镜头'); +}; + +const isTimePlanSection = (sectionKey: string): boolean => { + const lower = sectionKey.toLowerCase(); + return lower.includes('time') || lower.includes('timeline') || lower.includes('时间规划') || lower.includes('动态时间规划'); +}; + +const pickFirstContent = (item: Record, keys: string[]): string => { + for (const key of keys) { + if (key in item && !isEmptyText(item[key])) return toText(item[key]); + } + return ''; +}; + +const normalizeFlowContent = (baseContent: string, extras: string[]): string => { + const cleanBase = baseContent.trim(); + const uniqueExtras = extras.filter((item, index) => item && extras.indexOf(item) === index); + if (uniqueExtras.length && (!cleanBase || PLACEHOLDER_FLOW_TEXTS.has(cleanBase))) { + return uniqueExtras.join(';'); + } + const parts = cleanBase && !EMPTY_TEXTS.has(cleanBase) ? [cleanBase] : []; + uniqueExtras.forEach((item) => { + if (item && !parts.includes(item)) parts.push(item); + }); + return parts.join(';') || '-'; +}; + +const normalizeFlowItemForDisplay = ( + item: Record, + contentKey: '动作内容' | '镜头内容', + contentKeys: string[], +): Record => { + const allowed = new Set(['时间段', contentKey, ...contentKeys]); + const extras: string[] = []; + Object.entries(item).forEach(([field, value]) => { + if (allowed.has(field)) return; + const fieldText = toText(field); + const valueText = toText(value); + if (fieldText && !EMPTY_TEXTS.has(fieldText)) extras.push(fieldText); + if (valueText && !EMPTY_TEXTS.has(valueText) && valueText !== fieldText) extras.push(valueText); + }); + return { + 时间段: toText(item['时间段']) || '-', + [contentKey]: normalizeFlowContent(pickFirstContent(item, contentKeys), extras), + }; +}; + +const normalizeRecordForSection = (sectionKey: string, item: Record): Record => { + if (isActionFlowSection(sectionKey)) { + return normalizeFlowItemForDisplay(item, '动作内容', ACTION_CONTENT_KEYS); + } + if (isCameraFlowSection(sectionKey)) { + return normalizeFlowItemForDisplay(item, '镜头内容', CAMERA_CONTENT_KEYS); + } + if (isTimePlanSection(sectionKey)) { + return { + 时间段: toText(item['时间段']) || '-', + 阶段: toText(item['阶段']) || '-', + 说明: toText(item['说明']) || '-', + }; + } + return item; +}; + +const renderValue = (value: unknown): React.ReactNode => { + if (value === null || value === undefined || value === '') return -; + if (typeof value === 'boolean') return value ? '是' : '否'; + if (typeof value === 'number') return value; + if (typeof value === 'string') { + return {value}; + } + return ; +}; + +const getArrayMode = (key: string): 'card' | 'table' => { + const lower = key.toLowerCase(); + if ( + lower.includes('action') || + lower.includes('motion') || + lower.includes('camera') || + lower.includes('shot') || + lower.includes('lens') || + lower.includes('flow') || + lower.includes('动作') || + lower.includes('镜头') + ) { + return 'card'; + } + return 'table'; +}; + +const renderCardArray = (sectionKey: string, items: Record[]): React.ReactNode => ( + + {items.map((item, index) => { + const displayItem = normalizeRecordForSection(sectionKey, item); + return ( + + + {Object.entries(displayItem).map(([field, value]) => ( + + {renderValue(value)} + + ))} + + + ); + })} + +); + +const renderTableArray = (sectionKey: string, items: Record[]): React.ReactNode => { + const displayItems = items.map(item => normalizeRecordForSection(sectionKey, item)); + const fields = Array.from(new Set(displayItems.flatMap(item => Object.keys(item)))); + const columns: ColumnsType> = fields.map(field => ({ + title: labelOf(field), + dataIndex: field, + key: field, + width: isLongText(displayItems.find(item => item[field])?.[field]) ? 280 : 160, + render: (value: unknown) => renderValue(value), + })); + + return ( +
`${sectionKey}-${index}`} + columns={columns} + dataSource={displayItems} + pagination={false} + scroll={{ x: Math.max(900, fields.length * 180) }} + tableLayout="fixed" + /> + ); +}; + +const renderArray = (sectionKey: string, value: unknown[]): React.ReactNode => { + if (!value.length) return ; + if (value.every(isRecord)) { + return getArrayMode(sectionKey) === 'card' + ? renderCardArray(sectionKey, value) + : renderTableArray(sectionKey, value); + } + return ; +}; + +const renderRecord = (value: Record): React.ReactNode => ( + + {Object.entries(value).map(([field, fieldValue]) => ( + + {Array.isArray(fieldValue) + ? renderArray(field, fieldValue) + : isRecord(fieldValue) + ? renderRecord(fieldValue) + : renderValue(fieldValue)} + + ))} + +); + +const renderSection = (key: string, value: unknown): React.ReactNode => { + if (Array.isArray(value)) return renderArray(key, value); + if (isRecord(value)) return renderRecord(value); + return renderValue(value); +}; + +interface VideoPromptSchemaViewerProps { + schema?: Record | null; + finalPrompt?: string | null; +} + +const VideoPromptSchemaViewer: React.FC = ({ schema, finalPrompt }) => { + const hasSchema = !!schema && Object.keys(schema).length > 0; + if (!hasSchema && !finalPrompt) { + return ; + } + + const schemaItems = hasSchema + ? Object.entries(schema || {}).map(([key, value]) => ({ + key, + label: labelOf(key), + children: renderSection(key, value), + })) + : []; + + const defaultKeys = finalPrompt ? ['final_prompt'] : []; + + return ( + + {finalPrompt ? ( + + {finalPrompt} + + ) : null} + + {schemaItems.length ? ( + + ) : null} + + ); +}; + +export default VideoPromptSchemaViewer; diff --git a/video-gen-admin/src/types/index.ts b/video-gen-admin/src/types/index.ts index 0f353139..aea94177 100644 --- a/video-gen-admin/src/types/index.ts +++ b/video-gen-admin/src/types/index.ts @@ -342,3 +342,242 @@ export interface GenerationAITaskQueryParams { userName?: string; } + + +// ── Admin Replication Readonly Types ────────────────────────────────────── + +export type ModuleReplicationStatus = + | 'pending' + | 'waiting_user' + | 'processing' + | 'completed' + | 'failed' + | 'cancelled' + | string; + +export interface ReplicationStepOut { + id: string; + projectId: string; + module: string; + stepIndex: number; + stepCode: string; + status: string; + version: number; + isCurrent: boolean; + parentStepId?: string | null; + sourceStepId?: string | null; + chatTaskId?: string | null; + input?: Record | null; + output?: Record | null; + errorMessage?: string | null; + createdAt?: string | null; + updatedAt?: string | null; + completedAt?: string | null; +} + +export interface ReplicationMaterialOut { + materialStepId?: string | null; + materialVideoUrl?: string | null; + materialImageUrl?: string | null; + sourceProjectName?: string | null; + targetProjectName?: string | null; + coreContentPoint?: string | null; +} + +export interface ReplicationImageGenerationOut { + promptStepId?: string | null; + generateStepId?: string | null; + prompt?: string | null; + engineId?: string | null; + engineName?: string | null; + params?: Record | null; + chatTaskId?: string | null; + status?: string | null; + resultImageUrl?: string | null; + errorMessage?: string | null; +} + +export interface ReplicationVideoGenerationOut { + promptStepId?: string | null; + generateStepId?: string | null; + promptSchema?: Record | null; + finalPrompt?: string | null; + promptParams?: Record | null; + engineId?: string | null; + engineName?: string | null; + params?: Record | null; + chatTaskId?: string | null; + status?: string | null; + resultVideoUrl?: string | null; + resultVideoCoverUrl?: string | null; + errorMessage?: string | null; +} + +export interface ReplicationProjectDetailOut { + id: string; + projectId: string; + userId?: string | null; + userName?: string | null; + module: string; + title?: string | null; + status: ModuleReplicationStatus; + currentStepCode?: string | null; + finalImageUrl?: string | null; + finalVideoUrl?: string | null; + finalVideoCoverUrl?: string | null; + errorMessage?: string | null; + material: ReplicationMaterialOut; + imageGeneration: ReplicationImageGenerationOut; + videoGeneration: ReplicationVideoGenerationOut; + steps: ReplicationStepOut[]; + createdAt?: string | null; + updatedAt?: string | null; + completedAt?: string | null; +} + +export interface HotOpeningTaskListItemOut { + id: string; + projectId: string; + userId?: string | null; + userName?: string | null; + module: string; + title?: string | null; + status: ModuleReplicationStatus; + currentStepCode?: string | null; + sourceProjectName?: string | null; + targetProjectName?: string | null; + coreContentPoint?: string | null; + finalImageUrl?: string | null; + finalVideoUrl?: string | null; + finalVideoCoverUrl?: string | null; + errorMessage?: string | null; + createdAt?: string | null; + updatedAt?: string | null; + completedAt?: string | null; +} + +export interface HotOpeningTaskListOut { + total: number; + items: HotOpeningTaskListItemOut[]; +} + +export interface AdminHotOpeningTaskQueryParams { + status?: string; + keyword?: string; + userId?: string; + userName?: string; + createdStart?: string; + createdEnd?: string; + page?: number; + pageSize?: number; +} + +export interface ShotTaskSetOut { + id: string; + userId?: string | null; + userName?: string | null; + title?: string | null; + videoUrl: string; + videoDurationSeconds: number; + status: string; + analysisStatus: string; + splitStatus: string; + originalVideoContent?: string | null; + originalVideoCategory?: string | null; + originalVideoAudience?: string | null; + segmentCount: number; + completedSegmentCount: number; + failedSegmentCount: number; + analysisErrorMessage?: string | null; + splitErrorMessage?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} + +export interface ShotAiSuggestionOut { + index: number; + startSecond: number; + endSecond: number; + durationSeconds: number; + timeNode: string; + content?: string | null; + category?: string | null; + audience?: string | null; +} + +export interface ShotTaskSetDetailOut extends ShotTaskSetOut { + aiSuggestions: ShotAiSuggestionOut[]; + analysisResultJson?: Record | any[] | null; +} + +export interface ShotTaskSetListOut { + total: number; + page: number; + pageSize: number; + items: ShotTaskSetOut[]; +} + +export interface AdminShotTaskSetQueryParams { + status?: string; + analysisStatus?: string; + splitStatus?: string; + keyword?: string; + userId?: string; + userName?: string; + createdStart?: string; + createdEnd?: string; + page?: number; + pageSize?: number; +} + +export interface ShotSegmentOut { + id: string; + taskSetId: string; + segmentIndex: number; + segmentName?: string | null; + sourceMode: string; + startSecond: number; + endSecond: number; + durationSeconds: number; + timeNode: string; + splitStatus: string; + analysisStatus: string; + replicateStatus: string; + segmentVideoUrl?: string | null; + originalVideoContent?: string | null; + originalVideoCategory?: string | null; + originalVideoAudience?: string | null; + segmentContent?: string | null; + segmentCategory?: string | null; + segmentAudience?: string | null; + splitRetryCount: number; + splitLastError?: string | null; + analysisErrorMessage?: string | null; + moduleProjectId?: string | null; + moduleProjectTitle?: string | null; + moduleProjectStatus?: string | null; + moduleProjectCurrentStepCode?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +} + +export interface ShotSegmentDetailOut extends ShotSegmentOut { + analysisJson?: Record | any[] | null; + aiSuggestionJson?: Record | any[] | null; +} + +export interface ShotSegmentListOut { + total: number; + page: number; + pageSize: number; + items: ShotSegmentOut[]; +} + +export interface AdminShotSegmentQueryParams { + sourceMode?: string; + splitStatus?: string; + analysisStatus?: string; + replicateStatus?: string; + page?: number; + pageSize?: number; +} diff --git a/video-gen-admin/tsconfig.tsbuildinfo b/video-gen-admin/tsconfig.tsbuildinfo index 6698c958..c1f0f8dc 100644 --- a/video-gen-admin/tsconfig.tsbuildinfo +++ b/video-gen-admin/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminsettings.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/store/index.ts","./src/types/index.ts","./src/utils/formatdate.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/store/index.ts","./src/types/index.ts","./src/utils/formatdate.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/video-gen-api/app/api/v1/hot_opening_replicate.py b/video-gen-api/app/api/v1/hot_opening_replicate.py index 2eab4212..0550b97b 100644 --- a/video-gen-api/app/api/v1/hot_opening_replicate.py +++ b/video-gen-api/app/api/v1/hot_opening_replicate.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from types import SimpleNamespace from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query @@ -241,12 +242,37 @@ async def create_task( ) async def list_tasks( status: ModuleProjectStatusEnum | None = Query(None, description="总任务状态筛选:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消;为空不过滤"), + keyword: str | None = Query(None, description="关键词搜索:项目ID、标题、生成项目名称、素材项目名称、核心内容点;普通用户只在自己的数据内搜索"), + user_id: str | None = Query(None, description="管理员专用:按用户ID筛选;普通用户不可使用"), + user_name: str | None = Query(None, description="管理员专用:按用户名模糊筛选;普通用户不可使用"), + created_start: datetime | None = Query(None, description="管理员专用:创建时间开始,ISO datetime;普通用户不可使用"), + created_end: datetime | None = Query(None, description="管理员专用:创建时间结束,ISO datetime;普通用户不可使用"), page: int = Query(1, ge=1, description="分页页码,从1开始"), page_size: int = Query(20, ge=1, le=100, description="每页数量,范围1-100"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): - return await list_hot_opening_projects(db, current_user=current_user, status=status.value if status else None, page=page, page_size=page_size) + admin_only_params = { + "user_id": user_id, + "user_name": user_name, + "created_start": created_start, + "created_end": created_end, + } + if not _safe_user_is_admin(current_user) and any(value is not None and str(value).strip() != "" for value in admin_only_params.values()): + raise HTTPException(status_code=403, detail="当前搜索条件仅管理员可用") + + return await list_hot_opening_projects( + db, + current_user=current_user, + status=status.value if status else None, + keyword=keyword, + user_id=user_id, + user_name=user_name, + created_start=created_start, + created_end=created_end, + page=page, + page_size=page_size, + ) @router.get( diff --git a/video-gen-api/app/api/v1/shot_replicate.py b/video-gen-api/app/api/v1/shot_replicate.py index 22b16923..d1720205 100644 --- a/video-gen-api/app/api/v1/shot_replicate.py +++ b/video-gen-api/app/api/v1/shot_replicate.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from types import SimpleNamespace from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query @@ -293,12 +294,25 @@ async def list_shot_task_sets( status: ShotTaskSetStatusEnum | None = Query(None, description="总任务状态筛选:pending_analysis=等待分析,analyzing=分析中,analysis_completed=分析完成,analysis_failed=分析失败,splitting=拆镜中,split_completed=拆镜完成,partial_failed=部分失败,failed=失败,deleted=已软删"), analysis_status: ShotAnalysisStatusEnum | None = Query(None, description="原视频分析状态筛选:pending=待分析,processing=分析中,completed=分析完成,failed=分析失败"), split_status: ShotSplitStatusEnum | None = Query(None, description="拆镜状态筛选:none=尚未拆镜,pending=待拆镜,processing=拆镜中,completed=拆镜完成,failed=拆镜失败,retry_waiting=等待恢复重试"), - keyword: str | None = Query(None, description="标题/原视频内容/分类/受众关键词,模糊搜索;为空不过滤"), + keyword: str | None = Query(None, description="标题/原视频内容/分类/受众关键词,模糊搜索;普通用户只在自己的数据内搜索"), + user_id: str | None = Query(None, description="管理员专用:按用户ID筛选;普通用户不可使用"), + user_name: str | None = Query(None, description="管理员专用:按用户名模糊筛选;普通用户不可使用"), + created_start: datetime | None = Query(None, description="管理员专用:创建时间开始,ISO datetime;普通用户不可使用"), + created_end: datetime | None = Query(None, description="管理员专用:创建时间结束,ISO datetime;普通用户不可使用"), page: int = Query(1, ge=1, description="分页页码,从1开始"), page_size: int = Query(20, ge=1, le=100, description="每页数量,范围1-100"), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): + admin_only_params = { + "user_id": user_id, + "user_name": user_name, + "created_start": created_start, + "created_end": created_end, + } + if not _safe_user_is_admin(current_user) and any(value is not None and str(value).strip() != "" for value in admin_only_params.values()): + raise HTTPException(status_code=403, detail="当前搜索条件仅管理员可用") + return await list_task_sets( db, current_user=current_user, @@ -306,6 +320,10 @@ async def list_shot_task_sets( analysis_status=analysis_status.value if analysis_status else None, split_status=split_status.value if split_status else None, keyword=keyword, + user_id=user_id, + user_name=user_name, + created_start=created_start, + created_end=created_end, page=page, page_size=page_size, ) diff --git a/video-gen-api/app/schemas/hot_opening_replicate.py b/video-gen-api/app/schemas/hot_opening_replicate.py index 0cc51459..8fd6d522 100644 --- a/video-gen-api/app/schemas/hot_opening_replicate.py +++ b/video-gen-api/app/schemas/hot_opening_replicate.py @@ -425,6 +425,8 @@ class HotOpeningVideoGenerationOut(BaseModel): class HotOpeningTaskDetailOut(BaseModel): id: str = Field(..., description="总任务项目ID。这个ID就是前端项目ID") project_id: str = Field(..., description="兼容前端命名,等同于 id") + user_id: str | None = Field(None, description="所属用户ID;管理员后台排查使用") + user_name: str | None = Field(None, description="所属用户名;管理员后台排查使用") module: str = Field(..., description="模块标识,爆款开头复刻固定为 hot_opening_replicate") title: str | None = Field(None, description="项目标题,默认取生成项目名称") status: str = Field(..., description="总任务状态:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消") @@ -445,11 +447,15 @@ class HotOpeningTaskDetailOut(BaseModel): class HotOpeningTaskListItemOut(BaseModel): id: str = Field(..., description="总任务项目ID。这个ID就是前端项目ID") project_id: str = Field(..., description="兼容前端命名,等同于 id") + user_id: str | None = Field(None, description="所属用户ID;管理员后台排查使用") + user_name: str | None = Field(None, description="所属用户名;管理员后台排查使用") module: str = Field(..., description="模块标识") title: str | None = Field(None, description="项目标题") status: str = Field(..., description="总任务状态:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消") current_step_code: str | None = Field(None, description="当前步骤") + source_project_name: str | None = Field(None, description="视频素材内容项目名称,来源于第1步素材输入") target_project_name: str | None = Field(None, description="生成项目名称,来源于第1步素材输入") + core_content_point: str | None = Field(None, description="核心内容点,来源于第1步素材输入") final_image_url: str | None = Field(None, description="最终图片 URL") final_video_url: str | None = Field(None, description="最终视频 URL") final_video_cover_url: str | None = Field(None, description="最终视频封面 URL") diff --git a/video-gen-api/app/schemas/shot_replicate.py b/video-gen-api/app/schemas/shot_replicate.py index 403e7cef..dbd38fe6 100644 --- a/video-gen-api/app/schemas/shot_replicate.py +++ b/video-gen-api/app/schemas/shot_replicate.py @@ -428,6 +428,8 @@ class ShotReplicateVideoGenerationOut(BaseModel): class ShotReplicateTaskDetailOut(BaseModel): id: str = Field(..., description="总任务项目ID。这个ID就是前端项目ID") project_id: str = Field(..., description="兼容前端命名,等同于 id") + user_id: str | None = Field(None, description="所属用户ID;管理员后台排查使用") + user_name: str | None = Field(None, description="所属用户名;管理员后台排查使用") module: str = Field(..., description="模块标识,拆镜复刻固定为 shot_replicate") title: str | None = Field(None, description="项目标题,默认取生成项目名称") status: str = Field(..., description="总任务状态:pending/waiting_user/processing/completed/failed/cancelled") @@ -579,6 +581,10 @@ class ShotTaskSetListQuery(BaseModel): analysis_status: str | None = Field(None, description="分析状态筛选:pending/processing/completed/failed") split_status: str | None = Field(None, description="拆镜状态筛选:none/pending/processing/completed/failed/retry_waiting") keyword: str | None = Field(None, description="标题/内容关键词,模糊搜索") + user_id: str | None = Field(None, description="管理员专用:用户ID筛选") + user_name: str | None = Field(None, description="管理员专用:用户名模糊筛选") + created_start: NaiveDatetimeOptional = Field(None, description="管理员专用:创建时间开始") + created_end: NaiveDatetimeOptional = Field(None, description="管理员专用:创建时间结束") page: int = Field(1, ge=1, description="页码") page_size: int = Field(20, ge=1, le=100, description="每页数量") @@ -587,6 +593,8 @@ class ShotTaskSetOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: str = Field(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id") + user_id: str | None = Field(None, description="所属用户ID;管理员后台排查使用") + user_name: str | None = Field(None, description="所属用户名;管理员后台排查使用") title: str | None = Field(None, description="拆镜总任务标题,可为空") video_url: str = Field(..., description="原视频 URL,来自已有上传接口") video_duration_seconds: float = Field(..., description="原视频时长,单位秒,允许浮点") @@ -659,6 +667,9 @@ class ShotSegmentOut(BaseModel): split_last_error: str | None = Field(None, description="最近一次切割失败原因") analysis_error_message: str | None = Field(None, description="片段分析失败原因") module_project_id: str | None = Field(None, description="由该片段创建的拆镜复刻项目ID,即 module_generation_projects.id") + module_project_title: str | None = Field(None, description="关联拆镜复刻项目标题;后台片段列表展示使用") + module_project_status: str | None = Field(None, description="关联拆镜复刻项目状态;后台片段列表展示使用") + module_project_current_step_code: str | None = Field(None, description="关联拆镜复刻项目当前步骤;后台片段列表展示使用") created_at: NaiveDatetimeOptional = Field(None, description="创建时间") updated_at: NaiveDatetimeOptional = Field(None, description="更新时间") diff --git a/video-gen-api/app/services/hot_opening_replicate_service.py b/video-gen-api/app/services/hot_opening_replicate_service.py index 2fbafdda..a0cc61c4 100644 --- a/video-gen-api/app/services/hot_opening_replicate_service.py +++ b/video-gen-api/app/services/hot_opening_replicate_service.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from typing import Any from fastapi import HTTPException -from sqlalchemy import func, select +from sqlalchemy import String, cast, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings @@ -486,9 +486,16 @@ async def project_to_detail_out(db: AsyncSession, project: ModuleGenerationProje video_url = video_generate_output.get("result_video_url") or (video_chat.video_url if video_chat else None) or project.final_video_url cover_url = video_generate_output.get("result_video_cover_url") or (video_chat.video_cover_url if video_chat else None) or project.final_video_cover_url + user_name: str | None = None + if project.user_id: + user_result = await db.execute(select(User.username).where(User.id == project.user_id).limit(1)) + user_name = user_result.scalar_one_or_none() + return HotOpeningTaskDetailOut( id=project.id, project_id=project.id, + user_id=project.user_id, + user_name=user_name, module=project.module, title=project.title, status=project.status, @@ -586,40 +593,141 @@ async def create_hot_opening_project(db: AsyncSession, current_user: User, req: return project +def _user_name_filter_subquery(value: str): + """后台按用户名筛选时使用的子查询。 + + 只在传入 user_name 时查询 users 表;keyword 不再关联 users,避免后台关键词搜索扩大查询范围。 + """ + like = f"%{value.strip()}%" + return select(User.id).where(User.username.ilike(like)) + + +async def _user_name_map_by_ids(db: AsyncSession, user_ids: set[str]) -> dict[str, str | None]: + """一次性查询当前页涉及的用户,避免列表逐条查询用户表。""" + if not user_ids: + return {} + result = await db.execute(select(User.id, User.username).where(User.id.in_(list(user_ids)))) + return {user_id: username for user_id, username in result.all()} + + +async def _current_step_map_by_project_ids( + db: AsyncSession, + *, + project_ids: set[str], + step_code: str, +) -> dict[str, ModuleGenerationStep]: + """一次性查询当前页项目的指定步骤,避免列表逐条查 material_input。""" + if not project_ids: + return {} + result = await db.execute( + select(ModuleGenerationStep).where( + ModuleGenerationStep.project_id.in_(list(project_ids)), + ModuleGenerationStep.step_code == step_code, + ModuleGenerationStep.is_current.is_(True), + ModuleGenerationStep.deleted_at.is_(None), + ) + ) + return {step.project_id: step for step in result.scalars().all()} + + async def list_hot_opening_projects( db: AsyncSession, *, current_user: User, status: str | None, + keyword: str | None = None, + user_id: str | None = None, + user_name: str | None = None, + created_start: datetime | None = None, + created_end: datetime | None = None, page: int, page_size: int, ) -> HotOpeningTaskListOut: + """ + 爆款开头列表查询。 + + 性能策略: + 1. 列表接口只查列表字段,不调用 project_to_detail_out,避免 steps/chat/user 多重 N+1。 + 2. 主表分页查询完成后,再按当前页 project_ids 批量查 material_input。 + 3. 管理员需要展示 user_name 时,按当前页 user_id 去重后一次性查 User。 + 4. user_name 使用 IN (SELECT users.id ...) 子查询筛选;keyword 不查询 users 表。 + """ query = select(ModuleGenerationProject).where( ModuleGenerationProject.module == MODULE, ModuleGenerationProject.deleted_at.is_(None), ) + if not current_user.is_admin: query = query.where(ModuleGenerationProject.user_id == current_user.id) + else: + if user_id and user_id.strip(): + query = query.where(ModuleGenerationProject.user_id == user_id.strip()) + if user_name and user_name.strip(): + query = query.where(ModuleGenerationProject.user_id.in_(_user_name_filter_subquery(user_name))) + if created_start: + query = query.where(ModuleGenerationProject.created_at >= created_start) + if created_end: + query = query.where(ModuleGenerationProject.created_at <= created_end) + if status: query = query.where(ModuleGenerationProject.status == status) - total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar_one() - result = await db.execute(query.order_by(ModuleGenerationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size)) - projects = list(result.scalars().all()) + if keyword and keyword.strip(): + like = f"%{keyword.strip()}%" + material_step_subquery = ( + select(ModuleGenerationStep.project_id).where( + ModuleGenerationStep.step_code == HotOpeningStepCodeEnum.MATERIAL_INPUT.value, + ModuleGenerationStep.is_current.is_(True), + ModuleGenerationStep.deleted_at.is_(None), + cast(ModuleGenerationStep.input_json, String).ilike(like), + ) + ) + query = query.where( + or_( + ModuleGenerationProject.id.ilike(like), + ModuleGenerationProject.title.ilike(like), + ModuleGenerationProject.current_step_code.ilike(like), + ModuleGenerationProject.error_message.ilike(like), + ModuleGenerationProject.id.in_(material_step_subquery), + ) + ) + + total = int((await db.execute(select(func.count()).select_from(query.subquery()))).scalar() or 0) + result = await db.execute( + query.order_by(ModuleGenerationProject.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + projects = list(result.scalars().unique().all()) + project_ids = {project.id for project in projects if project.id} + + material_step_map = await _current_step_map_by_project_ids( + db, + project_ids=project_ids, + step_code=HotOpeningStepCodeEnum.MATERIAL_INPUT.value, + ) + user_name_map: dict[str, str | None] = {} + if current_user.is_admin: + user_ids = {project.user_id for project in projects if project.user_id} + user_name_map = await _user_name_map_by_ids(db, user_ids) items: list[HotOpeningTaskListItemOut] = [] for project in projects: - material_step = await _get_current_step_by_code(db, project.id, HotOpeningStepCodeEnum.MATERIAL_INPUT.value) + material_step = material_step_map.get(project.id) material = _step_payload(material_step.input_json if material_step else None) items.append( HotOpeningTaskListItemOut( id=project.id, project_id=project.id, + user_id=project.user_id, + user_name=user_name_map.get(project.user_id) if current_user.is_admin and project.user_id else None, module=project.module, title=project.title, status=project.status, current_step_code=project.current_step_code, + source_project_name=material.get("source_project_name"), target_project_name=material.get("target_project_name"), + core_content_point=material.get("core_content_point"), final_image_url=build_resource_signed_url(project.final_image_url) if project.final_image_url else None, final_video_url=build_resource_signed_url(project.final_video_url) if project.final_video_url else None, final_video_cover_url=build_resource_signed_url(project.final_video_cover_url) if project.final_video_cover_url else None, diff --git a/video-gen-api/app/services/hot_opening_video_prompt_service.py b/video-gen-api/app/services/hot_opening_video_prompt_service.py index 218a0921..bec47701 100644 --- a/video-gen-api/app/services/hot_opening_video_prompt_service.py +++ b/video-gen-api/app/services/hot_opening_video_prompt_service.py @@ -341,33 +341,162 @@ def fill_none_with_wu(value: Any) -> Any: return value +TOP_LEVEL_SCHEMA_KEYS = tuple(CLIENT_SCHEMA_V1.keys()) + ("动态时间规划", "输出规格限制") + +OBJECT_FIELD_WHITELISTS: dict[str, set[str]] = { + key: set(value.keys()) + for key, value in CLIENT_SCHEMA_V1.items() + if isinstance(value, dict) +} +OBJECT_FIELD_WHITELISTS.setdefault("画面属性", set()).add("推荐分辨率") +OBJECT_FIELD_WHITELISTS["输出规格限制"] = {"支持时长", "支持比例", "支持分辨率", "当前推荐分辨率"} + +ACTION_FLOW_CONTENT_KEYS = ("动作内容", "动作", "动作说明", "内容", "说明", "主体动作", "动作变化") +CAMERA_FLOW_CONTENT_KEYS = ("镜头内容", "镜头", "镜头说明", "运镜", "运镜说明", "内容", "说明") +TIME_PLAN_ALLOWED_KEYS = ("时间段", "阶段", "说明") +PLACEHOLDER_FLOW_TEXTS = { + "展示主体动作、核心卖点或主要视觉内容", + "展示主要动作、核心卖点或主要视觉内容", + "展示核心卖点或主要视觉内容", + "展示主体动作", + "无", +} +EMPTY_VALUE_TEXTS = {"", "无", "null", "None", "none", "未提及", "不适用"} + + +def _clean_schema_text(value: Any) -> str: + if value is None: + return "" + if isinstance(value, (dict, list)): + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + return str(value).strip() + + +def _is_empty_schema_value(value: Any) -> bool: + return _clean_schema_text(value) in EMPTY_VALUE_TEXTS + + +def _normalize_schema_object(section_key: str, value: Any) -> dict[str, Any]: + default_value = CLIENT_SCHEMA_V1.get(section_key) + if not isinstance(default_value, dict): + default_value = {} + source = value if isinstance(value, dict) else {} + merged = copy.deepcopy(default_value) + allowed_keys = OBJECT_FIELD_WHITELISTS.get(section_key, set(default_value.keys())) + for field_key in allowed_keys: + if field_key in source: + merged[field_key] = fill_none_with_wu(source.get(field_key)) + return merged + + +def normalize_top_level_schema_fields(result: dict[str, Any]) -> dict[str, Any]: + """按客户端视频提词 schema 白名单清洗顶层和普通对象字段。 + + AI 偶尔会把解释性文本作为 JSON key 输出。普通对象中的非预设字段直接过滤, + 动作流程/镜头流程这类列表字段在后续 flow normalize 中单独处理。 + """ + source = result if isinstance(result, dict) else {} + normalized: dict[str, Any] = {} + for key in TOP_LEVEL_SCHEMA_KEYS: + if key in OBJECT_FIELD_WHITELISTS: + normalized[key] = _normalize_schema_object(key, source.get(key)) + elif key in source: + normalized[key] = fill_none_with_wu(source.get(key)) + elif key in CLIENT_SCHEMA_V1: + normalized[key] = copy.deepcopy(CLIENT_SCHEMA_V1[key]) + for key, default_value in CLIENT_SCHEMA_V1.items(): + if key not in normalized: + normalized[key] = copy.deepcopy(default_value) + return normalized + + def ensure_top_keys(result: dict[str, Any]) -> dict[str, Any]: - schema = copy.deepcopy(CLIENT_SCHEMA_V1) - for key, default_value in schema.items(): - if key not in result: - result[key] = default_value - elif isinstance(default_value, dict) and isinstance(result.get(key), dict): - merged = copy.deepcopy(default_value) - merged.update(result[key]) - result[key] = merged - return result + return normalize_top_level_schema_fields(result) + + +def _pick_flow_content(item: dict[str, Any], content_keys: tuple[str, ...]) -> str: + for key in content_keys: + if key in item and not _is_empty_schema_value(item.get(key)): + return _clean_schema_text(item.get(key)) + return "" + + +def _collect_extra_flow_texts( + item: dict[str, Any], + *, + content_keys: tuple[str, ...], + allowed_keys: set[str], +) -> list[str]: + extras: list[str] = [] + for key, value in item.items(): + if key in allowed_keys or key in content_keys: + continue + key_text = _clean_schema_text(key) + value_text = _clean_schema_text(value) + if key_text and key_text not in EMPTY_VALUE_TEXTS: + extras.append(key_text) + if value_text and value_text not in EMPTY_VALUE_TEXTS and value_text != key_text: + extras.append(value_text) + # 去重但保留顺序,避免 AI 重复写入同一句。 + deduped: list[str] = [] + for text in extras: + if text not in deduped: + deduped.append(text) + return deduped + + +def _merge_flow_content(base_content: str, extra_texts: list[str], fallback: str) -> str: + base_content = _clean_schema_text(base_content) + if extra_texts and (not base_content or base_content in PLACEHOLDER_FLOW_TEXTS or base_content == fallback): + return ";".join(extra_texts) + parts = [base_content] if base_content and base_content not in EMPTY_VALUE_TEXTS else [] + for text in extra_texts: + if text and text not in parts: + parts.append(text) + return ";".join(parts) if parts else fallback + + +def _normalize_flow_item( + raw_item: Any, + *, + plan_item: dict[str, str], + content_key: str, + content_keys: tuple[str, ...], +) -> dict[str, str]: + item = raw_item if isinstance(raw_item, dict) else {} + allowed_keys = {"时间段", content_key} + base_content = _pick_flow_content(item, content_keys) + extra_texts = _collect_extra_flow_texts(item, content_keys=content_keys, allowed_keys=allowed_keys) + fallback = plan_item.get("说明") or "无" + return { + "时间段": plan_item.get("时间段") or _clean_schema_text(item.get("时间段")) or "无", + content_key: _merge_flow_content(base_content, extra_texts, fallback), + } + + +def _normalize_time_plan(plan: list[dict[str, str]], value: Any) -> list[dict[str, str]]: + source = value if isinstance(value, list) else [] + normalized: list[dict[str, str]] = [] + for index, plan_item in enumerate(plan): + raw_item = source[index] if index < len(source) and isinstance(source[index], dict) else {} + item: dict[str, str] = { + "时间段": plan_item.get("时间段") or _clean_schema_text(raw_item.get("时间段")) or "无", + "阶段": _clean_schema_text(raw_item.get("阶段")) or plan_item.get("阶段") or "无", + "说明": _clean_schema_text(raw_item.get("说明")) or plan_item.get("说明") or "无", + } + # 动态时间规划只保留 时间段/阶段/说明,多余字段不入库。 + normalized.append(item) + return normalized def ensure_flow_matches_time_plan(result: dict[str, Any], duration: int) -> dict[str, Any]: plan = build_time_plan(duration) - if not isinstance(result.get("动作流程"), list) or not result["动作流程"]: - result["动作流程"] = [ - {"时间段": item["时间段"], "动作内容": item["说明"]} - for item in plan - ] - if not isinstance(result.get("镜头流程"), list) or not result["镜头流程"]: - result["镜头流程"] = [ - {"时间段": item["时间段"], "镜头内容": item["说明"]} - for item in plan - ] - result["动作流程"] = _align_flow_time_ranges(result["动作流程"], plan, "动作内容") - result["镜头流程"] = _align_flow_time_ranges(result["镜头流程"], plan, "镜头内容") - result["动态时间规划"] = plan + result["动作流程"] = _align_flow_time_ranges(result.get("动作流程"), plan, "动作内容") + result["镜头流程"] = _align_flow_time_ranges(result.get("镜头流程"), plan, "镜头内容") + result["动态时间规划"] = _normalize_time_plan(plan, result.get("动态时间规划")) return result @@ -462,16 +591,20 @@ def clean_final_prompt_specs(schema: dict[str, Any], video_config: dict[str, Any return schema -def _align_flow_time_ranges(flow: Any, plan: list[dict[str, str]], default_content_key: str) -> list[dict[str, Any]]: +def _align_flow_time_ranges(flow: Any, plan: list[dict[str, str]], default_content_key: str) -> list[dict[str, str]]: source = flow if isinstance(flow, list) else [] - aligned: list[dict[str, Any]] = [] + content_keys = ACTION_FLOW_CONTENT_KEYS if default_content_key == "动作内容" else CAMERA_FLOW_CONTENT_KEYS + aligned: list[dict[str, str]] = [] for index, plan_item in enumerate(plan): - old_item = source[index] if index < len(source) and isinstance(source[index], dict) else {} - item = dict(old_item) - item["时间段"] = plan_item["时间段"] - if not any(k in item and str(item.get(k)).strip() for k in (default_content_key, "动作", "镜头", "说明", "内容")): - item[default_content_key] = plan_item["说明"] - aligned.append(item) + raw_item = source[index] if index < len(source) else {} + aligned.append( + _normalize_flow_item( + raw_item, + plan_item=plan_item, + content_key=default_content_key, + content_keys=content_keys, + ) + ) return aligned diff --git a/video-gen-api/app/services/shot_replicate_flow_service.py b/video-gen-api/app/services/shot_replicate_flow_service.py index 20a27745..0d000938 100644 --- a/video-gen-api/app/services/shot_replicate_flow_service.py +++ b/video-gen-api/app/services/shot_replicate_flow_service.py @@ -493,9 +493,16 @@ async def project_to_detail_out(db: AsyncSession, project: ModuleGenerationProje video_url = video_generate_output.get("result_video_url") or (video_chat.video_url if video_chat else None) or project.final_video_url cover_url = video_generate_output.get("result_video_cover_url") or (video_chat.video_cover_url if video_chat else None) or project.final_video_cover_url + user_name: str | None = None + if project.user_id: + user_result = await db.execute(select(User.username).where(User.id == project.user_id).limit(1)) + user_name = user_result.scalar_one_or_none() + return ShotReplicateTaskDetailOut( id=project.id, project_id=project.id, + user_id=project.user_id, + user_name=user_name, module=project.module, title=project.title, status=project.status, @@ -593,6 +600,26 @@ async def create_shot_replicate_project(db: AsyncSession, current_user: User, re return project +async def _current_step_map_by_project_ids( + db: AsyncSession, + *, + project_ids: set[str], + step_code: str, +) -> dict[str, ModuleGenerationStep]: + """一次性查询当前页项目的指定步骤,避免列表逐条查 material_input。""" + if not project_ids: + return {} + result = await db.execute( + select(ModuleGenerationStep).where( + ModuleGenerationStep.project_id.in_(list(project_ids)), + ModuleGenerationStep.step_code == step_code, + ModuleGenerationStep.is_current.is_(True), + ModuleGenerationStep.deleted_at.is_(None), + ) + ) + return {step.project_id: step for step in result.scalars().all()} + + async def list_shot_replicate_projects( db: AsyncSession, *, @@ -601,6 +628,11 @@ async def list_shot_replicate_projects( page: int, page_size: int, ) -> ShotReplicateTaskListOut: + """ + 拆镜复刻项目列表查询。 + + 这个列表目前不是后台主入口,但仍避免逐条查 material_input,保持和爆款开头列表一致的批量查询策略。 + """ query = select(ModuleGenerationProject).where( ModuleGenerationProject.module == MODULE, ModuleGenerationProject.deleted_at.is_(None), @@ -610,13 +642,23 @@ async def list_shot_replicate_projects( if status: query = query.where(ModuleGenerationProject.status == status) - total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar_one() - result = await db.execute(query.order_by(ModuleGenerationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size)) - projects = list(result.scalars().all()) + total = int((await db.execute(select(func.count()).select_from(query.subquery()))).scalar() or 0) + result = await db.execute( + query.order_by(ModuleGenerationProject.created_at.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + ) + projects = list(result.scalars().unique().all()) + project_ids = {project.id for project in projects if project.id} + material_step_map = await _current_step_map_by_project_ids( + db, + project_ids=project_ids, + step_code=ShotReplicateStepCodeEnum.MATERIAL_INPUT.value, + ) items: list[ShotReplicateTaskListItemOut] = [] for project in projects: - material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value) + material_step = material_step_map.get(project.id) material = _step_payload(material_step.input_json if material_step else None) items.append( ShotReplicateTaskListItemOut( diff --git a/video-gen-api/app/services/shot_replicate_taskset_service.py b/video-gen-api/app/services/shot_replicate_taskset_service.py index 2ad87bf4..af9c4c91 100644 --- a/video-gen-api/app/services/shot_replicate_taskset_service.py +++ b/video-gen-api/app/services/shot_replicate_taskset_service.py @@ -7,7 +7,7 @@ from typing import Any from fastapi import HTTPException from app.config import settings -from sqlalchemy import func, select +from sqlalchemy import String, cast, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.enums.shot_replicate import ( @@ -19,6 +19,7 @@ from app.enums.shot_replicate import ( ShotSplitStatusEnum, ShotTaskSetStatusEnum, ) +from app.models.module_generation_project import ModuleGenerationProject from app.models.shot_replicate_segment import ShotReplicateSegment from app.models.shot_replicate_task_set import ShotReplicateTaskSet from app.models.user import User @@ -85,26 +86,37 @@ def _normalize_suggestions(value: Any) -> list[dict[str, Any]]: return normalized -def _task_set_to_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetOut: - return ShotTaskSetOut.model_validate(task_set) +def _task_set_to_out(task_set: ShotReplicateTaskSet, user_name: str | None = None) -> ShotTaskSetOut: + data = ShotTaskSetOut.model_validate(task_set) + data.user_name = user_name + return data -def _task_set_to_detail_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetDetailOut: +def _task_set_to_detail_out(task_set: ShotReplicateTaskSet, user_name: str | None = None) -> ShotTaskSetDetailOut: suggestions = [ShotAISuggestionOut(**{k: v for k, v in item.items() if k != "raw"}) for item in _normalize_suggestions(task_set.ai_suggestion_json)] base = ShotTaskSetDetailOut.model_validate(task_set) + base.user_name = user_name base.ai_suggestions = suggestions return base -def _segment_to_out(segment: ShotReplicateSegment) -> ShotSegmentOut: +def _segment_to_out(segment: ShotReplicateSegment, project: ModuleGenerationProject | None = None) -> ShotSegmentOut: data = ShotSegmentOut.model_validate(segment) data.segment_name = f"片段{segment.segment_index}" + if project: + data.module_project_title = project.title + data.module_project_status = project.status + data.module_project_current_step_code = project.current_step_code return data -def _segment_to_detail_out(segment: ShotReplicateSegment) -> ShotSegmentDetailOut: +def _segment_to_detail_out(segment: ShotReplicateSegment, project: ModuleGenerationProject | None = None) -> ShotSegmentDetailOut: data = ShotSegmentDetailOut.model_validate(segment) data.segment_name = f"片段{segment.segment_index}" + if project: + data.module_project_title = project.title + data.module_project_status = project.status + data.module_project_current_step_code = project.current_step_code return data @@ -201,6 +213,23 @@ async def create_task_set(db: AsyncSession, *, current_user: User, req: ShotTask return task_set +def _user_name_filter_subquery(value: str): + """后台按用户名筛选时使用的子查询。 + + 只在传入 user_name 时查询 users 表;keyword 不再关联 users,避免后台关键词搜索扩大查询范围。 + """ + like = f"%{value.strip()}%" + return select(User.id).where(User.username.ilike(like)) + + +async def _user_name_map_by_ids(db: AsyncSession, user_ids: set[str]) -> dict[str, str | None]: + """一次性查询当前页涉及的用户,避免列表逐条查询用户表。""" + if not user_ids: + return {} + result = await db.execute(select(User.id, User.username).where(User.id.in_(list(user_ids)))) + return {user_id: username for user_id, username in result.all()} + + async def list_task_sets( db: AsyncSession, *, @@ -209,39 +238,85 @@ async def list_task_sets( analysis_status: str | None = None, split_status: str | None = None, keyword: str | None = None, + user_id: str | None = None, + user_name: str | None = None, + created_start: datetime | None = None, + created_end: datetime | None = None, page: int = 1, page_size: int = 20, ) -> ShotTaskSetListOut: + """ + 拆镜总任务集列表查询。 + + 性能策略: + 1. 主列表不 join User,避免 count/list 复杂化。 + 2. 管理员按 user_name 搜索时使用 IN (SELECT users.id ...) 子查询;keyword 不查询 users 表。 + 3. 当前页数据取出后,再按 user_id 去重批量查 user_name,用于后台渲染。 + """ query = select(ShotReplicateTaskSet).where(ShotReplicateTaskSet.deleted_at.is_(None)) + if not current_user.is_admin: query = query.where(ShotReplicateTaskSet.user_id == current_user.id) + else: + if user_id and user_id.strip(): + query = query.where(ShotReplicateTaskSet.user_id == user_id.strip()) + if user_name and user_name.strip(): + query = query.where(ShotReplicateTaskSet.user_id.in_(_user_name_filter_subquery(user_name))) + if created_start: + query = query.where(ShotReplicateTaskSet.created_at >= created_start) + if created_end: + query = query.where(ShotReplicateTaskSet.created_at <= created_end) + if status: query = query.where(ShotReplicateTaskSet.status == status) if analysis_status: query = query.where(ShotReplicateTaskSet.analysis_status == analysis_status) if split_status: query = query.where(ShotReplicateTaskSet.split_status == split_status) - if keyword: + if keyword and keyword.strip(): like = f"%{keyword.strip()}%" - query = query.where( - (ShotReplicateTaskSet.title.ilike(like)) - | (ShotReplicateTaskSet.original_video_content.ilike(like)) - | (ShotReplicateTaskSet.original_video_category.ilike(like)) - ) + conditions = [ + ShotReplicateTaskSet.id.ilike(like), + ShotReplicateTaskSet.title.ilike(like), + ShotReplicateTaskSet.original_video_content.ilike(like), + ShotReplicateTaskSet.original_video_category.ilike(like), + ShotReplicateTaskSet.original_video_audience.ilike(like), + cast(ShotReplicateTaskSet.ai_suggestion_json, String).ilike(like), + ] + query = query.where(or_(*conditions)) total_result = await db.execute(select(func.count()).select_from(query.subquery())) total = int(total_result.scalar() or 0) - rows = await db.execute( + result = await db.execute( query.order_by(ShotReplicateTaskSet.created_at.desc()) .offset((page - 1) * page_size) .limit(page_size) ) - return ShotTaskSetListOut(total=total, page=page, page_size=page_size, items=[_task_set_to_out(item) for item in rows.scalars().all()]) + task_sets = list(result.scalars().unique().all()) + + user_name_map: dict[str, str | None] = {} + if current_user.is_admin: + user_ids = {task_set.user_id for task_set in task_sets if task_set.user_id} + user_name_map = await _user_name_map_by_ids(db, user_ids) + + return ShotTaskSetListOut( + total=total, + page=page, + page_size=page_size, + items=[ + _task_set_to_out( + task_set, + user_name_map.get(task_set.user_id) if current_user.is_admin and task_set.user_id else None, + ) + for task_set in task_sets + ], + ) async def task_set_detail(db: AsyncSession, *, current_user: User, task_set_id: str) -> ShotTaskSetDetailOut: task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user) - return _task_set_to_detail_out(task_set) + user_result = await db.execute(select(User.username).where(User.id == task_set.user_id).limit(1)) + return _task_set_to_detail_out(task_set, user_result.scalar_one_or_none()) async def _next_segment_index(db: AsyncSession, task_set_id: str) -> int: @@ -499,9 +574,13 @@ async def list_segments( page_size: int = 20, ) -> ShotSegmentListOut: await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user) - query = select(ShotReplicateSegment).where( - ShotReplicateSegment.task_set_id == task_set_id, - ShotReplicateSegment.deleted_at.is_(None), + query = ( + select(ShotReplicateSegment, ModuleGenerationProject) + .outerjoin(ModuleGenerationProject, ModuleGenerationProject.id == ShotReplicateSegment.module_project_id) + .where( + ShotReplicateSegment.task_set_id == task_set_id, + ShotReplicateSegment.deleted_at.is_(None), + ) ) if not current_user.is_admin: query = query.where(ShotReplicateSegment.user_id == current_user.id) @@ -521,9 +600,18 @@ async def list_segments( .offset((page - 1) * page_size) .limit(page_size) ) - return ShotSegmentListOut(total=total, page=page, page_size=page_size, items=[_segment_to_out(item) for item in rows.scalars().all()]) + return ShotSegmentListOut( + total=total, + page=page, + page_size=page_size, + items=[_segment_to_out(segment, project) for segment, project in rows.all()], + ) async def segment_detail(db: AsyncSession, *, current_user: User, segment_id: str) -> ShotSegmentDetailOut: segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user) - return _segment_to_detail_out(segment) + project: ModuleGenerationProject | None = None + if segment.module_project_id: + project_result = await db.execute(select(ModuleGenerationProject).where(ModuleGenerationProject.id == segment.module_project_id).limit(1)) + project = project_result.scalar_one_or_none() + return _segment_to_detail_out(segment, project)