From 37abdf8cfdbf6619a615438b7bc0071f4aa8a339 Mon Sep 17 00:00:00 2001 From: Miguel <miguel.vera.csa@gmail.com> Date: Sat, 8 Feb 2025 19:17:08 +0100 Subject: [PATCH] intentando hacer funcionar los 3 niveles --- backend/app.py | 166 ++- .../config_manager.cpython-310.pyc | Bin 0 -> 9986 bytes .../profile_manager.cpython-310.pyc | Bin 4192 -> 6466 bytes .../script_manager.cpython-310.pyc | Bin 5780 -> 7115 bytes backend/core/config_manager.py | 297 +++++ backend/core/profile_manager.py | 188 ++- backend/core/script_manager.py | 222 ++-- backend/script_groups/base_script.py | 37 +- backend/script_groups/config.json | 49 +- backend/script_groups/data.json | 12 + claude/__init__.py | 1 - claude/__init___1.py | 1 - claude/__init___2.py | 1 - claude/__init___3.py | 1 - claude/app.py | 193 --- claude/base.html | 15 - claude/base_script.py | 61 - claude/claude_file_organizer.py | 172 --- claude/config.json | 30 - claude/config_1.json | 32 - claude/directory_handler.py | 23 - claude/group_settings_manager.py | 122 -- claude/index.html | 115 -- claude/main.js | 264 ---- claude/modal.js | 39 - claude/profile.js | 324 ----- claude/profile_manager.py | 133 --- claude/profiles.json | 26 - claude/project_structure.txt | 39 - claude/script_manager.py | 189 --- claude/scripts.js | 815 ------------- claude/style.css | 31 - claude/workdir_config.js | 161 --- claude/workdir_config.py | 72 -- claude/x1.py | 108 -- claude/x2.py | 104 -- data/profile_schema.json | 39 + data/profiles.json | 10 +- frontend/static/css/style.css | 9 +- frontend/static/js/main.js | 246 ++-- frontend/static/js/profile.js | 145 ++- frontend/static/js/scripts.js | 1058 ++++++----------- frontend/static/js/utils.js | 3 + frontend/static/js/workdir_config.js | 206 +--- frontend/templates/base.html | 9 + frontend/templates/index.html | 374 ++++-- 46 files changed, 1630 insertions(+), 4512 deletions(-) create mode 100644 backend/core/__pycache__/config_manager.cpython-310.pyc create mode 100644 backend/core/config_manager.py create mode 100644 backend/script_groups/data.json delete mode 100644 claude/__init__.py delete mode 100644 claude/__init___1.py delete mode 100644 claude/__init___2.py delete mode 100644 claude/__init___3.py delete mode 100644 claude/app.py delete mode 100644 claude/base.html delete mode 100644 claude/base_script.py delete mode 100644 claude/claude_file_organizer.py delete mode 100644 claude/config.json delete mode 100644 claude/config_1.json delete mode 100644 claude/directory_handler.py delete mode 100644 claude/group_settings_manager.py delete mode 100644 claude/index.html delete mode 100644 claude/main.js delete mode 100644 claude/modal.js delete mode 100644 claude/profile.js delete mode 100644 claude/profile_manager.py delete mode 100644 claude/profiles.json delete mode 100644 claude/project_structure.txt delete mode 100644 claude/script_manager.py delete mode 100644 claude/scripts.js delete mode 100644 claude/style.css delete mode 100644 claude/workdir_config.js delete mode 100644 claude/workdir_config.py delete mode 100644 claude/x1.py delete mode 100644 claude/x2.py create mode 100644 data/profile_schema.json create mode 100644 frontend/static/js/utils.js diff --git a/backend/app.py b/backend/app.py index 1bf0035..88975d2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,11 +4,11 @@ import sys from pathlib import Path # Add the parent directory to Python path -backend_dir = Path(__file__).parent.parent # Sube un nivel más para incluir la carpeta raíz +backend_dir = Path(__file__).parent.parent if str(backend_dir) not in sys.path: sys.path.append(str(backend_dir)) -from flask import Flask, render_template, jsonify, request, send_from_directory +from flask import Flask, render_template, jsonify, request from core.directory_handler import select_directory from core.script_manager import ScriptManager from core.profile_manager import ProfileManager @@ -46,9 +46,8 @@ def get_profile(profile_id): @app.route('/api/profiles', methods=['POST']) def create_profile(): """Create new profile""" - profile_data = request.json try: - profile = profile_manager.create_profile(profile_data) + profile = profile_manager.create_profile(request.json) return jsonify(profile) except Exception as e: return jsonify({"error": str(e)}), 400 @@ -57,13 +56,9 @@ def create_profile(): def update_profile(profile_id): """Update existing profile""" try: - profile_data = request.json - print(f"Received update request for profile {profile_id}: {profile_data}") # Debug - profile = profile_manager.update_profile(profile_id, profile_data) - print(f"Profile updated: {profile}") # Debug + profile = profile_manager.update_profile(profile_id, request.json) return jsonify(profile) except Exception as e: - print(f"Error updating profile: {e}") # Debug return jsonify({"error": str(e)}), 400 @app.route('/api/profiles/<profile_id>', methods=['DELETE']) @@ -75,6 +70,7 @@ def delete_profile(profile_id): except Exception as e: return jsonify({"error": str(e)}), 400 +# Script group endpoints @app.route('/api/script-groups', methods=['GET']) def get_script_groups(): """Get all available script groups""" @@ -84,110 +80,94 @@ def get_script_groups(): except Exception as e: return jsonify({"error": str(e)}), 500 -# Directory handling endpoints -@app.route('/api/select-directory', methods=['GET']) -def handle_select_directory(): - """Handle directory selection""" - print("Handling directory selection request") # Debug - result = select_directory() - print(f"Directory selection result: {result}") # Debug - if "error" in result: - return jsonify(result), 400 - return jsonify(result) - -# Script management endpoints -@app.route('/api/scripts', methods=['GET']) -def get_scripts(): - """Get all available script groups""" +@app.route('/api/script-groups/<group_id>/config', methods=['GET']) +def get_group_config(group_id): + """Get script group configuration""" try: - groups = script_manager.discover_groups() - return jsonify(groups) + config = script_manager.get_group_data(group_id) + return jsonify(config) except Exception as e: return jsonify({"error": str(e)}), 500 -@app.route('/api/scripts/<group_id>/<script_id>/run', methods=['POST']) -def run_script(group_id, script_id): - """Execute a specific script""" - data = request.json - work_dir = data.get('work_dir') - profile = data.get('profile') - - if not work_dir: - return jsonify({"error": "Work directory not specified"}), 400 - if not profile: - return jsonify({"error": "Profile not specified"}), 400 - +@app.route('/api/script-groups/<group_id>/config', methods=['PUT']) +def update_group_config(group_id): + """Update script group configuration""" try: - result = script_manager.execute_script(group_id, script_id, work_dir, profile) - return jsonify(result) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# Work directory configuration endpoints -@app.route('/api/workdir-config/<path:work_dir>', methods=['GET']) -def get_workdir_config(work_dir): - """Get work directory configuration""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - return jsonify(config_manager.get_config()) - -@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['GET']) -def get_group_config(work_dir, group_id): - """Get group configuration from work directory""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - return jsonify(config_manager.get_group_config(group_id)) - -@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['PUT']) -def update_group_config(work_dir, group_id): - """Update group configuration in work directory""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - - try: - settings = request.json - config_manager.update_group_config(group_id, settings) - return jsonify({"status": "success"}) + config = script_manager.update_group_data(group_id, request.json) + return jsonify(config) except Exception as e: return jsonify({"error": str(e)}), 400 -@app.route('/api/script-groups/<group_id>/config-schema', methods=['PUT']) -def update_group_config_schema(group_id): - """Update configuration schema for a script group""" - try: - schema = request.json - config_file = Path(script_manager.script_groups_dir) / group_id / "config.json" - - with open(config_file, 'w', encoding='utf-8') as f: - json.dump(schema, f, indent=4) - - return jsonify({"status": "success"}) - except Exception as e: - return jsonify({"error": str(e)}), 500 - @app.route('/api/script-groups/<group_id>/scripts', methods=['GET']) def get_group_scripts(group_id): """Get scripts for a specific group""" try: - print(f"Loading scripts for group: {group_id}") # Debug scripts = script_manager.get_group_scripts(group_id) - print(f"Scripts found: {scripts}") # Debug return jsonify(scripts) except Exception as e: - print(f"Error loading scripts: {str(e)}") # Debug return jsonify({"error": str(e)}), 500 -@app.route('/api/script-groups/<group_id>/config-schema', methods=['GET']) -def get_group_config_schema(group_id): - """Get configuration schema for a script group""" +@app.route('/api/script-groups/<group_id>/schema', methods=['GET']) +def get_group_schema(group_id): + """Get script group schema""" try: - print(f"Loading config schema for group: {group_id}") # Debug - schema = script_manager.get_group_config_schema(group_id) - print(f"Schema loaded: {schema}") # Debug + schema = script_manager.get_global_schema() return jsonify(schema) except Exception as e: - print(f"Error loading schema: {str(e)}") # Debug + return jsonify({"error": str(e)}), 500 + +# Directory handling endpoints +@app.route('/api/select-directory', methods=['GET']) +def handle_select_directory(): + """Handle directory selection""" + result = select_directory() + if "error" in result: + return jsonify(result), 400 + return jsonify(result) + +# Work directory configuration endpoints +@app.route('/api/workdir-config/<group_id>', methods=['GET']) +def get_workdir_config(group_id): + """Get work directory configuration for a group""" + try: + group_data = script_manager.get_group_data(group_id) + work_dir = group_data.get('work_dir') + + if not work_dir: + return jsonify({"error": "Work directory not configured"}), 400 + + from core.workdir_config import WorkDirConfigManager + workdir_manager = WorkDirConfigManager(work_dir, group_id) + return jsonify(workdir_manager.get_group_config()) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/workdir-config/<group_id>', methods=['PUT']) +def update_workdir_config(group_id): + """Update work directory configuration for a group""" + try: + group_data = script_manager.get_group_data(group_id) + work_dir = group_data.get('work_dir') + + if not work_dir: + return jsonify({"error": "Work directory not configured"}), 400 + + from core.workdir_config import WorkDirConfigManager + workdir_manager = WorkDirConfigManager(work_dir, group_id) + workdir_manager.update_group_config(request.json) + return jsonify({"status": "success"}) + except Exception as e: + return jsonify({"error": str(e)}), 400 + +# Script execution endpoint +@app.route('/api/script-groups/<group_id>/scripts/<script_id>/run', methods=['POST']) +def run_script(group_id, script_id): + """Execute a specific script""" + try: + result = script_manager.execute_script(group_id, script_id, request.json.get('profile', {})) + return jsonify(result) + except Exception as e: return jsonify({"error": str(e)}), 500 if __name__ == '__main__': - app.run(debug=True, port=5000) + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/backend/core/__pycache__/config_manager.cpython-310.pyc b/backend/core/__pycache__/config_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc863169fc3505ce2a03185cb0004dfc2389c8c7 GIT binary patch literal 9986 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 GIQM^Fs0d;J literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/profile_manager.cpython-310.pyc b/backend/core/__pycache__/profile_manager.cpython-310.pyc index 0851fe0e97b2fc966f5e2e19c0132a446daea676..052daa28dea78f72b5a1f7f613b61901678abb8e 100644 GIT binary patch literal 6466 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 WeIrxin+)CbJwnBBk;PTmy!k&a=MmWe literal 4192 zcmZ`+ZExGw73RI9D4LdK#c^UgZ)LNzfZ8R+fV~u++a-&WVnONzOWR<A2ZGV196A)K zUQ%&Q$VJfv*r#CM_GRs0e=71<`V;Qg0R#FMGN9>mE-6KFQwqGiFXvt!p7WgN9Hk2j zu7T^be|)#|pOc31Um8qbCI(k=XSdJ@L$KKBvkqg_=P~baJk8kbTOF%k>(u&o$L`lV zb-ixIPT%P`%=pL<HDNzBg#Cnf<^;cIwCg|MJfqE&bvMW#ZnZ6CZART(nb(qiW!;Xl zTse0Jd6XtW3@Jwhd6-B2uuI#fF9(CGxHF3xMrd@HFrXCcn1TyaSoC61|HedX3+J0! z#}ad4t;dBc=AT*}TQtIY=!EqiUo$YfAexw+t7hG5c2Tr2J1^c8OCjX7<VmsowANXG zWK*A@W%bG_aT?N#VkLC2TAn{%wZy7e!)i;Md9`ciZI(DI&OzdY-v3Kdwm2^?Kx&Dk zp!XZX!|0?q0n=Vo&AT$~MRE9XkOVuSEH0Ft%<HB}FWMQ(fDG#mN~uiMMc505ab7HL zmL0D;ps4K(^2@*7>FMi#e^N%<RJDF8!niY^hy6i_qlPl<)Pq6fKMMETHN_*LtR(1% zsu9P1KMV6bN_H~kc4Y`B3O~q|I~<Thb)Q@t_#%>BKIK>H(|w&>djZXejSL^HU}p{V zFgoN%)H8*FkLFv&!uWfx>}F}ww&V$HQ<GVrETWozoCd-liz;W_Yjb5~VcgTZo*VKs zHow>u>)SBJe%Q^^Z2MkUMuR-tzMXc1xEkCKA8bDex{ty{Y<E)`Zdd2|{nGYV2K&nK z{V0iY-$#HMnT^I~E^D%41!qtB>B?A6*)oJ#lP(}Vxh(W3R<%XGM`vX08i(x2kavoA za!w<`2=vd)e=Z!b+&r{K?1&xk`-ZSA!!n+j%t)Nyz?^%CU;6zGx2TQyV<s=do;BE$ zi`oX-`>?5H<o5m+GY+t?IGNXlJu?4oe9NCu^w&!Hh}}1AoFl9Q^Gowy_1t57TDx6m zcTUvtbDr_(JqOVC&>it3oHQafm@8Ip!z~j;dOex;J?$_iU(|+q@ACW4!2)5@O$E@j zCFfv(V)cfUsr1OBn4Iih_dbC)>w`dsNuFWX7oZbAebk<pOW3AtJ&`FZ9fXOp2rbH@ zL)402g4C}9VvG|tcjJq0sOd=61~N)=WtYbz8Xf_=z*QDJ4#)Bt(07Vn#d>mCKSK?u zvRT25BrL4rΜHIcH0(!Op#O%m%Z8jV4=V&R6`$rCNi#%wom)<5-!&@eERkv+34I zc?X>_Qh*A6XdW4d){y}paEd1KHl7?$22Ys9Ti11*dX)*rR`vFx{Gmq_oaI4(AQy42 zY@tEOFK*trdF|8NpZItGdgta}Zr@N=Hy!NDI#!jLq<d;E%F<pc`$66|OT*NwIFVQA z916B6+g!r%GVYA<?(zn6@PiHBK5m28duvPEe1=YDg6Cg2l|FrF$U2$kfEDlMFai2} z4Dj5fQJFyyP^|-=Te~%o5T|_d3?YHAVl!d&?sQn;Jcj~>q^eN5RfUp+wYgWJ^fe?X zcqnMvZgJ`!a5Of0=H;mlgRNp2^V1;5%DSi6o;-`Mk>}9-Kp%-*X-A0w@QbsRwK9*O zDZFgh?S@&_8^-Z|u~LQ(R;CYK_u6%BJoz@AO3@<Uq=rDMEHQ-R-$J*7cPUB;lkZY< z%#Cf#{SJ3_293jQ##sw6UjodVoFL9M#D5NL(svR{GteeNoItyYK~CUilnR~@d|A=@ zD9pVejwdEW#<F4%KpBYk;~*Y}S(|AX9@AL0cEa4p=E=+dOzKk}YVzWZW9U0BtK16H zek5kVNBbvAH!vX39Wr8Pt@1LdoDITm)Qh@fz1zH0*u-m|^HSUQvG=&j^Q1=g;+t64 z+BSG`<~7<@VyZD#lv3}Esx_QIFw)fV6deIIksh2J<=zwaHAaH1mE-}tKQ`nVR?HFN zMp)mPBr!!X=E#IRB@}wv8-}QF8h>9nutrEtIQ921L~UfPQF6C7jL+MY<aZHc*kK&n zM@Ubal$>IzBBdnU8yiTHB_}D=>kqTsdq}YpBwi$5IMw%i#UkJUN)!8D$@^2(#pMDH zkp!f6UAdnTHgsT=$R+|S3F<xS{hAsAb-PyjV}mBNQ;5Zsi2~@jdzuwRRTjSmIbF^; z9GnJbZ?T3+?B<|FKz!w5{xxhrdrFtkG``4X#FN@(-=kC9g-hTlxC9&zLbHg7O4GvV zt2t;*uT~Bt7d_(g67oBTLoZ>yt{sUA0=c5B3LJ6?Tm$EmV-Dcj!+Mp$i7K(OfnWr& zRZ?Rjg(Ds9$ZEc-EavtF=nyq~LC|tX)>s~NIDcS$X=yra6f2+VO0jg^6jc=`^upAP zS2W5TMW9~y<OeXt3)f51-0P*oMCg*C1kJCm2MNu09|p-z2()-<?|8n`xvhE&$g}d_ zx>u}~%o!zKS;UTmZ{6E!yCrW@%9kHfa|}V0)RbNB(?}}Ww1SgH5@D*d*d0nLUdCs? zj$T!B_Wc1iW|z=#YZ>Kbi?slymf192jF!=F=(6G$ICu7}8BQSjns5Sr6>$InGsOwy zbU^Er<^lrV7+TutA5b)dp{WD};n_uVGmOK0lI6-`?tJAG{I{UBW95f-r@V%@Z=r<H zw$)aqaFpxR%qAbzEYBHRa0HQz7=r@O<t+qX<L5=y?4CB;YA;juPXA_nU%9@Izdpm5 z`VHUzd>F*lO5OKG+V%aCBFJEpYCB1Bs!Ze_UG7P;q9hkd`V&wBfWlY)fto*3bCntj zWw}YstB=w>T{+S}Ub-_P9mlX3H#uC)@w5NmbWYb!pIUa#EL^ZIT6_hwFR}L0<?fG5 zXC>`xyHp$J598>8)T&;@q_Xq<0isE(Tq>$H(~4>o)7IDNqnwS>Dj!mE)+HMiY^%WA GlKB&a6ZNYA diff --git a/backend/core/__pycache__/script_manager.cpython-310.pyc b/backend/core/__pycache__/script_manager.cpython-310.pyc index f9f5dbb58825d25199f075222683f728cf01bfe7..de69dd61ebc8b7ffc1393d65420459dde3baf5a6 100644 GIT binary patch literal 7115 zcmaJ_TaO&ab?)l>Tz7W4ye)4{+7>g4L`Q~`$OJ>ku|y$(vQoT?6_05RdZ&7qJ?y!# zx`)ec^e~L5D2Naxc?=K{*aZRvde5Ipz2-@OAP=%(<vUe9vopiBX3(drt4>|d?K?GU zG^z%E$-n>Ce)oo9{4aeh{w#dFg(vNRaD%hN7_bgwR5lZ{W2v{@vGKMNXW(|+f!FZ{ ze#akFI+c>I+Nq-5PHKZ%r^bwT4eoIFQ-izD%}$-0J4W048si&nChZTx?7?>1miF6m zFO&9rahgf%r^7?(zdz36(J)NF<?}F$vUm{nsN3R?w(}OA^b=+nk<nq?=$M>gp=M<7 zncU*`r*_BTPUJ>j<nNgm4b-~aLv4lok+;-h^9rw`rK(zL3w>-}=MA*f_(JVU-zHxJ zN1eAY>U!k!jg$W1^!I#zhHrwSfv@ICn_AIXehw{br>y8azkrt3>WY@;cKJoV1+Mk7 z|MKWIzr-)2b%S4tZ1g*`G`G#K@@r__RDA=i{rXA!(kwo|!M_L2vrDxm|33c#YR~ak zAlRF-vC|XrIQy$`816?RZ|g!Dq^dgD7o*8I4Z<N0vIkKtg6K)qn~<dCx@PHWmM)J) z&s>l-mFf8ziF*^IV1>~&3gd_!gLX}BU;%8qZHWq6<(Y0W3VC-w8SREiH|;%$2BB;& zf45C(r%|$}dVFPw20DK8Gk*L22V!&>^|DcVUvDS9|K6w<CgsOR(eC}-u=g+;^83A! zi0+pwALvcr8bf`%-FO&h-ENJRYlFC~$?|i0gz|K5&6uPsqHH3DJ!tX#N1aH&)rLPq zp)mR|$xFtR<w3>@#+815Vf|}!YG&q<RhY-@Bb*U;Y{NF5Tg><fNAiY|UOj*b)Bl%v zK0^Br+B5s`&zUg=SH7OXKnv?Xj2Gr}GIlREVvOX1pF30QskKwS57>dx_VXXThm#HT z$p*dAa4+7Uh>(N`G1EB*dn2(d@w}$_ZuL`$nu%|mNw)X;n_t1WqhW8v<Kg~xe&rn@ z(2?YZ@7V-;$lHNz=pjm3e&uR8+oN$bl(rhzrmnK`&XZoGyoYqgA|7UKThwuWothr1 zyB8;sWP6fDO5&O{!6E3p*YGF>tC}WjF`xaxw@tIjHktp-{9S&1MS`a&nXfds8}lXo zbujEp;w&IeB~nv~)Lam$r9^7|9+3v$AyOM6T~i{pXCjRu_EMboQ(K7>ZzWDIU(@0w z%M9RY!*41?m88TvoyrE0Ga%df){2Pcoo)x>95~vJI%;thWvRwdr!6ki5Lbwhjh&ph zY?Wq819fTMi)dHJ-&{I=2!f9P>Z!*+?<{Q^qDOIT_H$8~pZmYzgUo1t?pFR<Vf9&` zP9Em;g~^P}DiAg(X2G;Jrgmocox&_^?s5MMWmLR!Vqm<YOzgE4d7u_1<{rKa6LTOB zKl6CCaAxxO#Nd^w^VCrisO4)r;p0gAMkN7p4eR{yD^q&oP(;Hlg_S%(XzY$2N_KD} zkFUu`AV9kDkb`YIzo^BNhL4p2El8*BYjbh+h|(E6gohR9F@U%Lf|HvJ#$t=UFVU#( zi6F%dstt&Iha53!q@A~t<M+Uxl1gYfw%7)HMH!NLx+GV+FWC;skRIg<1qvj?BgR>5 z9Gg=!h8ru)BlFmJ+xT_+zB#q0PGJ}9u_3M_K3G&P?2AU>97DE+eaCpzC>->M#E(t! z!_4M(0bAkD3+p-igsC?7n9X_N0NY-E;}>BPL%$X!6@>d?JWR9F@rkooob*NXXcCKv z-^*J+FN?um93?!snb&UyaT*NbG=+$y$D_S)l4P(OiWJh?k22}RSu{xHTK91|A#4bp zWfcpUL`uXn@sMPsTSm9ajGs&EVRR_#s$1zVR}F9TWOVJXl+k0jl+3+p+N@>z>;kL9 z2ewSx%x|oi-fXf3nTtQB<v3`WMB-0S?vaAdejW;QOZ-cOy23hw_d8}&yMXmgjI@Eb z_0SZ7YJm+xI+rqr;KU#uuapk;@{ie$kteXq3iw~m%mSf{!X0X=sJY8)x(+(<6IgWF znqj6nUmbiq8EiW<pPOPE8sr`uyz$VaaWZd#vkA^|=2Hzc%Q>nX8!3kl!lDt5QPZ#S zHT+x3f0%_cpJN?8KZp0AHE--gTOTv=kJ!<f!o-d?)xOTY06(-&&8sPVNB!_W;X7RT zjym|yDbDlQllvTJ@xrDa+?={kUFAbA<X4~^T06slR4a)}l~dA?c6K<9wxuCnhZOGR z-n~*C8%iB#3M+1*75bDRZ0<{UI2r6lLb6-Z+Z~OPC`6#m#3X`ehk64aiNg;C89KT| z2`Z#FQh_(kTYoWB3KHlFC_ZK)SWw!*H|%D9amJ&TxY(LoH*W{+hWH`I7C)j1EUZa7 zdkD8k5D?R4HO4N}EFAVC=_I2Atkb4N3Id6;7N^~CHytIDEK-h7yh=lnduw~zvyrZf zH;Is@%BnUNIwon4k(QhmuEZN){WCo2ZIG(jGRZuu%!H{_0Tlu~RRBFI(|=ZOv2)C2 zE?~q3@_yguSAL%{sa5n=aUl5y<@_F_unz>N9iVb}Z%m>5<ov{&h1s``Y%;0zO*lTN zb{_x&&e1{}J96n8s_1d_Kntm*M>$eia^@vuNXI7kzjTg)2OoI_jICYCue}>(L6{^# z_&AJ{a2G0;F2Z`ry3!togGknRq+5Uq1s#?24@Tl4fqLmhL+Z*uY&+77x!6Xtc%R4z zME)9N7OXrtLP338h?Bw<@yaTsF0%9>hK#OLg&U560lCta66;A<rxCJJIyVebJ@Iod zIYYSJ?tE8e3i2_=PATBo3jWoZt^RdLmHz&3%x5;D!m=b!8)zw(k$PHqlsL&9LQudX z<j>kHzf4E0*0}<#$vd~1+Hc!j2+b>Ss!8*o>~o0mQ<|}A=9gby=n3NzWFn_ck6Pke zD5xbOyO>%TBTYN9kaAg^-Juo)SLoXX`bLOF=B;WSgj{GCLk%`&<=6gZybwB<lLp$G ztWFdI@4~>VJrN<!QW=5nPq6|)!9!ZZ(SfYQ=?IQw5N7H$#A|3NrLJJ7LMyVVypI}$ zhF=x<x}I&}s0dc1lqOWImQ_XAt6Mh9uPq<iOTCsvOAuv#ZhQEcK}}_$NK*v@9Gzc! zHp6Ktmu~8y2Ad`bV*`*dfS4viBH6EvGe>w4ufkGYz!$Q{%Lq*hpwgAf7wS4C4@hma z#ih!RhCw=xdhuS|)7q3b^+rnFx|!E*2E$PX-pP>Xl_$UYqt}1(Yb35qo`<{;DbEg` z)K~i<8AG1tw)i=|)u{r1vL~zO>)%Pj!7dN){xkKZL3|^BWBD*oRJC0Btvd;2+38&r zbZ=-px#^ida1{zp#fLPv3JW%6p+HK=ypzK`0@k712k{FqASR6jlxPN(C?yep3)1n* z`6LoTVpk~&Wryk`9zwfT%xe9mwLZf@WtIqh#K=Wd;T{kQp0&(XwRx$xc!2Ipx<j5| z4vv~Q`8>{yKCG=^@Y#%DuKEUmQ6AmwJJ1Md3-WaUsLVU^=?l7Z0RY&Ux~QoX0I}l7 zg$tl!^=p74KhElf%L(qFc8WxC$R1rTY_SjShSD~#@WlItchuBz;bZ9E)GvJTUg2}+ zBKWyG^@p(TzBNUPgf?2AbF>DJ;1kVSh&1ca{EcJwoN@0<o8S~~9pK;qYt9^g23Uiu zLfksw&PmxorKo5~L$?#Sb70fT(+ctHk^(r@{PIt=j05Pl#y47EKokj(jU^_ySg3wY zN-GqhxC6(a6m=eCq)qJk+2uU894R+}I?lqw48~BMB;J)a?xqrXo{XuxCq@IMDo9y- z>be8BM?mVbH5l<p5@`lvl`dTi0Fl(_daJ9Or3bu%Mz}ZV!yqDPm7+53C1IM%YPaO; zcIozr!YE4KgD?#<Kpz)<_^2nHbezOU5eTA<#oO4TtcfVWa*%xikkQ@OLKaump@P&6 zi&aZ2S5q(5UCpiot2Nr~1KN#x8vp5_d`i+T$hZ9qp7bt=sZbE2qqPC`uLJd%-`YqY z5FO1br4h*dT)b@y2ng87|E>Ff&Gk(xQAWRtgqAH?R%TYokqnCw32!N>XkrC(yEzBB z&%!A@s7)!N_MqvR3@HX_8{KgMY#U@%mkb5NtrsR>yj!?OHhl9Rf)iFo>7e3QP&>bu zIeqsCNdauioO<fZFFcG6+OHf{3#25lMR!`sYDabYDs8bkt)Zq-)L@Gr7qxz~@cV19 z#gAbrHSWO{QA-v9TfA3P(g&Cysm6SzmFLE^UesYz4n<V5CfL!&;pecU2JEQ8E7_T% z0b8qniB@&Z<4zk-8?YbsE-x7_zN4;MBeEYTf{G3La24>%Z*GqQ0%^e<+s`3L#8pty zxFoj_CvtB`Bfos}B#s2}u#7L;q7S*`*D$2c0`x?IQZ^lQh*>%j-T2`e7AZ%VO;Yh0 zy0tfTV4s73t<)S%f&h&e5g8Mqpe<4&8IcJQg2>{4$P*%mL~<guByAdGHtpsDiu#nA z{(;CZi6|gSHY5Iy$TK2;Pei$qPf)^wb)-4PJY-W}YR{t4e}g}zM578^p|B*fg-zrP zO=J(J+f7sD3QcoMXA3LPZF%^YLN+-+1>Fdb5Cl=kQi1pHxU)s5r7Qz40zsz-z{R`t z{{fc@?C$B|ZjpDpeO}9fBqHnG?xRVV%sgDT3y;(7s=Q6C(cBghl98ZGQ*|kO0`)W; zq&!U6L^?!%MI<B=6QPYOCnf%gN}m%siRh|CenSwZp(iD5;2Sn;n&<4T>Q?ne?Z(>H z^{v(`_SNRq=Bux4{Xp@y8+0R=JwRm8X=ZaCDQ;1q(CSHAC|x9yh*EklYN2W`YGoI| gpa88J^t6>p|BiOkcS)EjEw*k@LW%SdDzjn!Kg_1|`~Uy| literal 5780 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 KiKoIA?EeBxBA*uk diff --git a/backend/core/config_manager.py b/backend/core/config_manager.py new file mode 100644 index 0000000..790ba76 --- /dev/null +++ b/backend/core/config_manager.py @@ -0,0 +1,297 @@ +# backend/core/config_manager.py +from pathlib import Path +import json +from typing import Dict, Any, Optional, List +from datetime import datetime + +class BaseConfigManager: + """Base class for all configuration managers""" + + def __init__(self, config_path: Path, schema_path: Optional[Path] = None): + self.config_path = Path(config_path) + self.schema_path = Path(schema_path) if schema_path else None + self._schema = None + self._config = None + + def _load_schema(self) -> Dict[str, Any]: + """Load configuration schema""" + if not self.schema_path or not self.schema_path.exists(): + return {"config_schema": {}} + + try: + with open(self.schema_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading schema: {e}") + return {"config_schema": {}} + + def _load_config(self) -> Dict[str, Any]: + """Load configuration data""" + if not self.config_path.exists(): + return {} + + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading config: {e}") + return {} + + def _save_config(self, config: Dict[str, Any]): + """Save configuration data""" + self.config_path.parent.mkdir(parents=True, exist_ok=True) + + try: + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4) + except Exception as e: + print(f"Error saving config: {e}") + raise + + def _validate_config(self, config: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]: + """Validate configuration against schema""" + validated = {} + schema_fields = schema.get("config_schema", {}) + + for key, field_schema in schema_fields.items(): + if key in config: + validated[key] = self._validate_field(key, config[key], field_schema) + elif field_schema.get("required", False): + raise ValueError(f"Required field '{key}' is missing") + else: + validated[key] = field_schema.get("default") + + return validated + + def _validate_field(self, key: str, value: Any, field_schema: Dict[str, Any]) -> Any: + """Validate a single field value""" + field_type = field_schema.get("type") + + if value is None or value == "": + if field_schema.get("required", False): + raise ValueError(f"Field '{key}' is required") + return field_schema.get("default") + + try: + if field_type == "string": + return str(value) + elif field_type == "number": + return float(value) if "." in str(value) else int(value) + elif field_type == "boolean": + if isinstance(value, str): + return value.lower() == "true" + return bool(value) + elif field_type == "directory": + path = Path(value) + if not path.is_absolute(): + path = Path(self.config_path).parent / path + path.mkdir(parents=True, exist_ok=True) + return str(path) + elif field_type == "select": + if value not in field_schema.get("options", []): + raise ValueError(f"Invalid option '{value}' for field '{key}'") + return value + else: + return value + except Exception as e: + raise ValueError(f"Invalid value for field '{key}': {str(e)}") + + def get_schema(self) -> Dict[str, Any]: + """Get configuration schema""" + if self._schema is None: + self._schema = self._load_schema() + return self._schema + + def get_config(self) -> Dict[str, Any]: + """Get current configuration""" + if self._config is None: + self._config = self._load_config() + return self._config + + def update_schema(self, schema: Dict[str, Any]): + """Update configuration schema""" + if not self.schema_path: + raise ValueError("No schema path configured") + + try: + self.schema_path.parent.mkdir(parents=True, exist_ok=True) + with open(self.schema_path, 'w', encoding='utf-8') as f: + json.dump(schema, f, indent=4) + self._schema = schema + except Exception as e: + print(f"Error saving schema: {e}") + raise + +class ProfileManager(BaseConfigManager): + """Manager for application profiles""" + + DEFAULT_PROFILE = { + "id": "default", + "name": "Default Profile", + "llm_settings": { + "model": "gpt-4", + "temperature": 0.7, + "api_key": "" + } + } + + def __init__(self, data_dir: Path): + super().__init__( + config_path=data_dir / "profiles.json", + schema_path=data_dir / "profile_schema.json" + ) + self.profiles = self._load_profiles() + + def _load_profiles(self) -> Dict[str, Dict]: + """Load all profiles""" + if self.config_path.exists(): + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + profiles = json.load(f) + if "default" not in profiles: + profiles["default"] = self._create_default_profile() + return profiles + except Exception as e: + print(f"Error loading profiles: {e}") + return {"default": self._create_default_profile()} + else: + profiles = {"default": self._create_default_profile()} + self._save_profiles(profiles) + return profiles + + def _create_default_profile(self) -> Dict[str, Any]: + """Create default profile""" + profile = self.DEFAULT_PROFILE.copy() + now = datetime.now().isoformat() + profile["created_at"] = now + profile["updated_at"] = now + return profile + + def _save_profiles(self, profiles: Dict[str, Dict]): + """Save all profiles""" + try: + self.config_path.parent.mkdir(parents=True, exist_ok=True) + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(profiles, f, indent=4) + except Exception as e: + print(f"Error saving profiles: {e}") + raise + + def get_all_profiles(self) -> List[Dict[str, Any]]: + """Get all profiles""" + return list(self.profiles.values()) + + def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: + """Get specific profile""" + return self.profiles.get(profile_id) + + def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: + """Create new profile""" + if "id" not in profile_data: + raise ValueError("Profile must have an id") + + profile_id = profile_data["id"] + if profile_id in self.profiles: + raise ValueError(f"Profile {profile_id} already exists") + + # Add timestamps + now = datetime.now().isoformat() + profile_data["created_at"] = now + profile_data["updated_at"] = now + + # Validate against schema + schema = self.get_schema() + validated_data = self._validate_config(profile_data, schema) + + # Add to profiles + self.profiles[profile_id] = validated_data + self._save_profiles(self.profiles) + return validated_data + + def update_profile(self, profile_id: str, profile_data: Dict[str, Any]) -> Dict[str, Any]: + """Update existing profile""" + if profile_id not in self.profiles: + raise ValueError(f"Profile {profile_id} not found") + + if profile_id == "default" and "id" in profile_data: + raise ValueError("Cannot change id of default profile") + + # Update timestamp + profile_data["updated_at"] = datetime.now().isoformat() + + # Validate against schema + schema = self.get_schema() + validated_data = self._validate_config(profile_data, schema) + + # Update profile + self.profiles[profile_id].update(validated_data) + self._save_profiles(self.profiles) + return self.profiles[profile_id] + + def delete_profile(self, profile_id: str): + """Delete profile""" + if profile_id == "default": + raise ValueError("Cannot delete default profile") + + if profile_id not in self.profiles: + raise ValueError(f"Profile {profile_id} not found") + + del self.profiles[profile_id] + self._save_profiles(self.profiles) + +class ScriptGroupManager(BaseConfigManager): + """Manager for script group configuration""" + + def __init__(self, group_dir: Path): + super().__init__( + config_path=group_dir / "data.json", + schema_path=group_dir.parent / "config.json" + ) + + def get_work_dir(self) -> Optional[str]: + """Get work directory from configuration""" + config = self.get_config() + return config.get("work_dir") + + def update_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: + """Update configuration""" + # Add timestamp + config_data["updated_at"] = datetime.now().isoformat() + + # Validate against schema + schema = self.get_schema() + validated_data = self._validate_config(config_data, schema) + + # Save configuration + self._save_config(validated_data) + self._config = validated_data + return validated_data + +class WorkDirConfigManager(BaseConfigManager): + """Manager for work directory configuration""" + + def __init__(self, work_dir: str, group_id: str): + self.work_dir = Path(work_dir) + self.group_id = group_id + super().__init__( + config_path=self.work_dir / "script_config.json", + schema_path=None # Schema is loaded from group configuration + ) + + def get_group_config(self) -> Dict[str, Any]: + """Get configuration for current group""" + config = self.get_config() + return config.get("group_settings", {}).get(self.group_id, {}) + + def update_group_config(self, settings: Dict[str, Any]): + """Update configuration for current group""" + config = self.get_config() + + if "group_settings" not in config: + config["group_settings"] = {} + + config["group_settings"][self.group_id] = settings + config["updated_at"] = datetime.now().isoformat() + + self._save_config(config) + self._config = config \ No newline at end of file diff --git a/backend/core/profile_manager.py b/backend/core/profile_manager.py index 3baa562..6ba30d5 100644 --- a/backend/core/profile_manager.py +++ b/backend/core/profile_manager.py @@ -1,4 +1,3 @@ -# backend/core/profile_manager.py from pathlib import Path import json from typing import Dict, Any, List, Optional @@ -6,44 +5,55 @@ from datetime import datetime class ProfileManager: - """Manages configuration profiles""" + """Manages application profiles""" DEFAULT_PROFILE = { "id": "default", "name": "Default Profile", "llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""}, - "created_at": "", - "updated_at": "", } def __init__(self, data_dir: Path): self.data_dir = data_dir self.profiles_file = data_dir / "profiles.json" + self.schema_file = data_dir / "profile_schema.json" self.profiles: Dict[str, Dict] = self._load_profiles() + self._schema = self._load_schema() + + def _load_schema(self) -> Dict[str, Any]: + """Load profile schema""" + if self.schema_file.exists(): + try: + with open(self.schema_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading profile schema: {e}") + return {"config_schema": {}} def _load_profiles(self) -> Dict[str, Dict]: - """Load profiles from file""" - if self.profiles_file.exists(): - try: + """Load all profiles and ensure default profile exists""" + profiles = {} + + # Crear perfil por defecto si no existe + default_profile = self._create_default_profile() + + try: + if self.profiles_file.exists(): with open(self.profiles_file, "r", encoding="utf-8") as f: - profiles = json.load(f) - # Ensure default profile exists - if "default" not in profiles: - profiles["default"] = self._create_default_profile() - return profiles - except Exception as e: - print(f"Error loading profiles: {e}") - return {"default": self._create_default_profile()} - else: - # Create directory if it doesn't exist - self.profiles_file.parent.mkdir(parents=True, exist_ok=True) - # Create default profile - profiles = {"default": self._create_default_profile()} + loaded_profiles = json.load(f) + profiles.update(loaded_profiles) + except Exception as e: + print(f"Error loading profiles: {e}") + + # Asegurar que existe el perfil por defecto + if "default" not in profiles: + profiles["default"] = default_profile self._save_profiles(profiles) - return profiles + + return profiles def _create_default_profile(self) -> Dict[str, Any]: - """Create default profile with timestamp""" + """Create default profile""" profile = self.DEFAULT_PROFILE.copy() now = datetime.now().isoformat() profile["created_at"] = now @@ -51,25 +61,87 @@ class ProfileManager: return profile def _save_profiles(self, profiles: Optional[Dict] = None): - """Save profiles to file""" + """Save all profiles""" if profiles is None: profiles = self.profiles try: - print(f"Saving profiles to: {self.profiles_file}") # Agregar debug + self.profiles_file.parent.mkdir(parents=True, exist_ok=True) with open(self.profiles_file, "w", encoding="utf-8") as f: json.dump(profiles, f, indent=4) - print("Profiles saved successfully") # Agregar debug except Exception as e: - print(f"Error saving profiles: {e}") # Agregar debug - raise # Re-lanzar la excepción para que se maneje arriba + print(f"Error saving profiles: {e}") + raise + + def _validate_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: + """Validate profile data against schema""" + schema = self._schema.get("config_schema", {}) + validated = {} + + for key, field_schema in schema.items(): + if key in profile_data: + validated[key] = self._validate_field( + key, profile_data[key], field_schema + ) + elif field_schema.get("required", False): + raise ValueError(f"Required field '{key}' is missing") + else: + validated[key] = field_schema.get("default") + + # Pass through non-schema fields + for key, value in profile_data.items(): + if key not in schema: + validated[key] = value + + return validated + + def _validate_field( + self, key: str, value: Any, field_schema: Dict[str, Any] + ) -> Any: + """Validate a single field value""" + field_type = field_schema.get("type") + + if value is None or value == "": + if field_schema.get("required", False): + raise ValueError(f"Field '{key}' is required") + return field_schema.get("default") + + try: + if field_type == "string": + return str(value) + elif field_type == "number": + return float(value) if "." in str(value) else int(value) + elif field_type == "boolean": + if isinstance(value, str): + return value.lower() == "true" + return bool(value) + elif field_type == "select": + if value not in field_schema.get("options", []): + raise ValueError(f"Invalid option '{value}' for field '{key}'") + return value + else: + return value + except Exception as e: + raise ValueError(f"Invalid value for field '{key}': {str(e)}") def get_all_profiles(self) -> List[Dict[str, Any]]: """Get all profiles""" return list(self.profiles.values()) def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: - """Get specific profile""" - return self.profiles.get(profile_id) + """Get specific profile with fallback to default""" + profile = self.profiles.get(profile_id) + + if not profile and profile_id != "default": + # Si no se encuentra el perfil y no es el perfil por defecto, + # intentar retornar el perfil por defecto + profile = self.profiles.get("default") + if not profile: + # Si tampoco existe el perfil por defecto, crearlo + profile = self._create_default_profile() + self.profiles["default"] = profile + self._save_profiles(self.profiles) + + return profile def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: """Create new profile""" @@ -85,41 +157,36 @@ class ProfileManager: profile_data["created_at"] = now profile_data["updated_at"] = now - # Ensure required fields - for key in ["name", "llm_settings"]: - if key not in profile_data: - profile_data[key] = self.DEFAULT_PROFILE[key] + # Validate profile data + validated_data = self._validate_profile(profile_data) - self.profiles[profile_id] = profile_data + # Save profile + self.profiles[profile_id] = validated_data self._save_profiles() - return profile_data + return validated_data def update_profile( self, profile_id: str, profile_data: Dict[str, Any] ) -> Dict[str, Any]: """Update existing profile""" - try: - print(f"Updating profile {profile_id} with data: {profile_data}") - if profile_id not in self.profiles: - raise ValueError(f"Profile {profile_id} not found") + if profile_id not in self.profiles: + raise ValueError(f"Profile {profile_id} not found") - if profile_id == "default" and "id" in profile_data: - raise ValueError("Cannot change id of default profile") + if profile_id == "default" and "id" in profile_data: + raise ValueError("Cannot change id of default profile") - # Update timestamp - profile_data["updated_at"] = datetime.now().isoformat() + # Update timestamp + profile_data["updated_at"] = datetime.now().isoformat() - # Update profile - current_profile = self.profiles[profile_id].copy() # Hacer una copia - current_profile.update(profile_data) # Actualizar la copia - self.profiles[profile_id] = current_profile # Asignar la copia actualizada + # Validate profile data + validated_data = self._validate_profile(profile_data) - print(f"Updated profile: {self.profiles[profile_id]}") - self._save_profiles() - return self.profiles[profile_id] - except Exception as e: - print(f"Error in update_profile: {e}") # Agregar debug - raise + # Update profile + current_profile = self.profiles[profile_id].copy() + current_profile.update(validated_data) + self.profiles[profile_id] = current_profile + self._save_profiles() + return current_profile def delete_profile(self, profile_id: str): """Delete profile""" @@ -131,3 +198,18 @@ class ProfileManager: del self.profiles[profile_id] self._save_profiles() + + def get_schema(self) -> Dict[str, Any]: + """Get profile schema""" + return self._schema + + def update_schema(self, schema: Dict[str, Any]): + """Update profile schema""" + try: + self.schema_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.schema_file, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=4) + self._schema = schema + except Exception as e: + print(f"Error saving schema: {e}") + raise diff --git a/backend/core/script_manager.py b/backend/core/script_manager.py index 03d83a6..e9c3bdd 100644 --- a/backend/core/script_manager.py +++ b/backend/core/script_manager.py @@ -1,131 +1,163 @@ -# backend/core/script_manager.py from pathlib import Path import importlib.util import inspect from typing import Dict, List, Any, Optional import json -from .group_settings_manager import GroupSettingsManager # Agregar esta importación - +from datetime import datetime class ScriptManager: + """Manages script groups and their execution""" + def __init__(self, script_groups_dir: Path): self.script_groups_dir = script_groups_dir - self.group_settings = GroupSettingsManager(script_groups_dir) + self._global_schema = self._load_global_schema() - def get_group_settings(self, group_id: str) -> Dict[str, Any]: - """Get settings for a script group""" - return self.group_settings.get_group_settings(group_id) + def _load_global_schema(self) -> Dict[str, Any]: + """Load global configuration schema for script groups""" + schema_file = self.script_groups_dir / "config.json" + try: + with open(schema_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading global schema: {e}") + return {"config_schema": {}} - def update_group_settings(self, group_id: str, settings: Dict[str, Any]): - """Update settings for a script group""" - return self.group_settings.update_group_settings(group_id, settings) + def _load_group_data(self, group_id: str) -> Dict[str, Any]: + """Load group data""" + data_file = self.script_groups_dir / group_id / "data.json" + try: + with open(data_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Error loading group data: {e}") + return {} - def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: - """Get configuration schema for a script group""" - config_file = self.script_groups_dir / group_id / "config.json" - print(f"Looking for config file: {config_file}") # Debug + def _save_group_data(self, group_id: str, data: Dict[str, Any]): + """Save group data""" + data_file = self.script_groups_dir / group_id / "data.json" + try: + data_file.parent.mkdir(parents=True, exist_ok=True) + with open(data_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + except Exception as e: + print(f"Error saving group data: {e}") + raise - if config_file.exists(): - try: - with open(config_file, "r", encoding="utf-8") as f: - schema = json.load(f) - print(f"Loaded schema: {schema}") # Debug - return schema - except Exception as e: - print(f"Error loading group config schema: {e}") # Debug - else: - print(f"Config file not found: {config_file}") # Debug + def _validate_group_data(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Validate group data against schema""" + schema = self._global_schema.get("config_schema", {}) + validated = {} - # Retornar un schema vacío si no existe el archivo - return {"group_name": group_id, "description": "", "config_schema": {}} + for key, field_schema in schema.items(): + if key in data: + validated[key] = self._validate_field(key, data[key], field_schema) + elif field_schema.get("required", False): + raise ValueError(f"Required field '{key}' is missing") + else: + validated[key] = field_schema.get("default") + + return validated + + def _validate_field(self, key: str, value: Any, field_schema: Dict[str, Any]) -> Any: + """Validate a single field value""" + field_type = field_schema.get("type") + + if value is None or value == "": + if field_schema.get("required", False): + raise ValueError(f"Field '{key}' is required") + return field_schema.get("default") + + try: + if field_type == "string": + return str(value) + elif field_type == "number": + return float(value) if "." in str(value) else int(value) + elif field_type == "boolean": + if isinstance(value, str): + return value.lower() == "true" + return bool(value) + elif field_type == "directory": + path = Path(value) + if not path.is_absolute(): + path = self.script_groups_dir / path + path.mkdir(parents=True, exist_ok=True) + return str(path) + elif field_type == "select": + if value not in field_schema.get("options", []): + raise ValueError(f"Invalid option '{value}' for field '{key}'") + return value + else: + return value + except Exception as e: + raise ValueError(f"Invalid value for field '{key}': {str(e)}") def get_available_groups(self) -> List[Dict[str, Any]]: - """Get list of available script groups""" + """Get all available script groups""" groups = [] - for group_dir in self.script_groups_dir.iterdir(): if group_dir.is_dir() and not group_dir.name.startswith("_"): - groups.append( - { + group_data = self._load_group_data(group_dir.name) + if group_data: + groups.append({ "id": group_dir.name, - "name": group_dir.name.replace("_", " ").title(), - "path": str(group_dir), - } - ) - + "name": group_data.get("name", group_dir.name), + "description": group_data.get("description", ""), + "work_dir": group_data.get("work_dir", ""), + "enabled": group_data.get("enabled", True) + }) return groups + def get_group_data(self, group_id: str) -> Dict[str, Any]: + """Get group configuration data""" + return self._load_group_data(group_id) + + def update_group_data(self, group_id: str, data: Dict[str, Any]) -> Dict[str, Any]: + """Update group configuration data""" + # Validar datos + validated_data = self._validate_group_data(data) + + # Actualizar timestamps + validated_data["updated_at"] = datetime.now().isoformat() + if not self._load_group_data(group_id): + validated_data["created_at"] = validated_data["updated_at"] + + # Guardar datos + self._save_group_data(group_id, validated_data) + return validated_data + def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: """Get scripts for a specific group""" group_dir = self.script_groups_dir / group_id - print(f"Looking for scripts in: {group_dir}") # Debug - if not group_dir.exists() or not group_dir.is_dir(): - print(f"Directory not found: {group_dir}") # Debug raise ValueError(f"Script group '{group_id}' not found") scripts = [] for script_file in group_dir.glob("x[0-9].py"): - print(f"Found script file: {script_file}") # Debug script_info = self._analyze_script(script_file) if script_info: scripts.append(script_info) return sorted(scripts, key=lambda x: x["id"]) - def discover_groups(self) -> List[Dict[str, Any]]: - """Discover all script groups""" - groups = [] - - for group_dir in self.script_groups_dir.iterdir(): - if group_dir.is_dir() and not group_dir.name.startswith("_"): - group_info = self._analyze_group(group_dir) - if group_info: - groups.append(group_info) - - return groups - - def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]: - """Analyze a script group directory""" - scripts = [] - - for script_file in group_dir.glob("x[0-9].py"): - try: - script_info = self._analyze_script(script_file) - if script_info: - scripts.append(script_info) - except Exception as e: - print(f"Error analyzing script {script_file}: {e}") - - if scripts: - return { - "id": group_dir.name, - "name": group_dir.name.replace("_", " ").title(), - "scripts": sorted(scripts, key=lambda x: x["id"]), - } - return None - def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]: """Analyze a single script file""" try: - # Import script module + # Importar módulo del script spec = importlib.util.spec_from_file_location(script_file.stem, script_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # Find script class + # Encontrar la clase del script script_class = None for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and hasattr(obj, "run") - ): + if (inspect.isclass(obj) and + obj.__module__ == module.__name__ and + hasattr(obj, "run")): script_class = obj break if script_class: - # Extraer la primera línea del docstring como nombre + # Extraer nombre y descripción del docstring docstring = inspect.getdoc(script_class) if docstring: name, *description = docstring.split("\n", 1) @@ -138,44 +170,38 @@ class ScriptManager: "id": script_file.stem, "name": name.strip(), "description": description.strip(), - "file": str(script_file.relative_to(self.script_groups_dir)), + "file": str(script_file.relative_to(self.script_groups_dir)) } except Exception as e: print(f"Error loading script {script_file}: {e}") + return None - return None - - def execute_script( - self, group_id: str, script_id: str, profile: Dict[str, Any] - ) -> Dict[str, Any]: + def execute_script(self, group_id: str, script_id: str, profile: Dict[str, Any]) -> Dict[str, Any]: """Execute a specific script""" - # Get group settings first - group_settings = self.group_settings.get_group_settings(group_id) - work_dir = group_settings.get("work_dir") + # Obtener datos del grupo + group_data = self._load_group_data(group_id) + work_dir = group_data.get("work_dir") if not work_dir: raise ValueError(f"No work directory configured for group {group_id}") script_file = self.script_groups_dir / group_id / f"{script_id}.py" - if not script_file.exists(): raise ValueError(f"Script {script_id} not found in group {group_id}") try: - # Import script module + # Importar módulo del script spec = importlib.util.spec_from_file_location(script_id, script_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # Find and instantiate script class + # Encontrar e instanciar la clase del script script_class = None for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and hasattr(obj, "run") - ): + if (inspect.isclass(obj) and + obj.__module__ == module.__name__ and + hasattr(obj, "run")): script_class = obj break @@ -187,3 +213,7 @@ class ScriptManager: except Exception as e: return {"status": "error", "error": str(e)} + + def get_global_schema(self) -> Dict[str, Any]: + """Get global configuration schema""" + return self._global_schema \ No newline at end of file diff --git a/backend/script_groups/base_script.py b/backend/script_groups/base_script.py index 2a34b9a..fae5720 100644 --- a/backend/script_groups/base_script.py +++ b/backend/script_groups/base_script.py @@ -2,6 +2,7 @@ from typing import Dict, Any from pathlib import Path import json +from datetime import datetime class BaseScript: """Base class for all scripts""" @@ -19,8 +20,8 @@ class BaseScript: """ raise NotImplementedError("Script must implement run method") - def get_config(self, work_dir: str, group_id: str) -> Dict[str, Any]: - """Get group configuration from work directory""" + def get_work_config(self, work_dir: str, group_id: str) -> Dict[str, Any]: + """Get configuration from work directory""" config_file = Path(work_dir) / "script_config.json" if config_file.exists(): @@ -29,33 +30,49 @@ class BaseScript: config = json.load(f) return config.get("group_settings", {}).get(group_id, {}) except Exception as e: - print(f"Error loading config: {e}") + print(f"Error loading work directory config: {e}") return {} - def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]): - """Save group configuration to work directory""" + def save_work_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]): + """Save configuration to work directory""" config_file = Path(work_dir) / "script_config.json" try: - # Load existing config or create new + # Cargar configuración existente o crear nueva if config_file.exists(): with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) else: config = { "version": "1.0", - "group_settings": {} + "group_settings": {}, + "created_at": datetime.now().isoformat() } - # Update settings + # Actualizar configuración if "group_settings" not in config: config["group_settings"] = {} config["group_settings"][group_id] = settings + config["updated_at"] = datetime.now().isoformat() - # Save config + # Guardar configuración with open(config_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=4) except Exception as e: - print(f"Error saving config: {e}") + print(f"Error saving work directory config: {e}") + raise + + def get_group_data(self, group_dir: Path) -> Dict[str, Any]: + """Get group configuration data""" + data_file = group_dir / "data.json" + + if data_file.exists(): + try: + with open(data_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error loading group data: {e}") + + return {} \ No newline at end of file diff --git a/backend/script_groups/config.json b/backend/script_groups/config.json index 4886062..cf7d8e6 100644 --- a/backend/script_groups/config.json +++ b/backend/script_groups/config.json @@ -2,29 +2,46 @@ "description": "Configuration schema for script groups", "config_schema": { "work_dir": { - "type": "string", + "type": "directory", "description": "Working directory for this script group", "required": true }, + "name": { + "type": "string", + "description": "Display name for the script group", + "required": true + }, "description": { "type": "string", - "description": "Description of this script group", + "description": "Detailed description of the script group", "default": "" }, - "backup_dir": { - "type": "directory", - "description": "Backup directory path", - "default": "" - }, - "max_files": { - "type": "number", - "description": "Maximum number of files to process", - "default": 1000 - }, - "enable_backup": { + "enabled": { "type": "boolean", - "description": "Enable automatic backups", - "default": false + "description": "Whether this script group is enabled", + "default": true + }, + "execution_mode": { + "type": "select", + "description": "Execution mode for scripts in this group", + "options": ["sequential", "parallel"], + "default": "sequential" + }, + "max_parallel": { + "type": "number", + "description": "Maximum number of parallel executions (if applicable)", + "default": 4 + }, + "timeout": { + "type": "number", + "description": "Script execution timeout in seconds", + "default": 3600 + }, + "logging": { + "type": "select", + "description": "Logging level for script execution", + "options": ["debug", "info", "warning", "error"], + "default": "info" } } -} +} \ No newline at end of file diff --git a/backend/script_groups/data.json b/backend/script_groups/data.json new file mode 100644 index 0000000..343788b --- /dev/null +++ b/backend/script_groups/data.json @@ -0,0 +1,12 @@ +{ + "name": "System Analysis", + "description": "Scripts for system analysis and file management", + "work_dir": "", + "enabled": true, + "execution_mode": "sequential", + "max_parallel": 4, + "timeout": 3600, + "logging": "info", + "created_at": "2025-02-08T12:00:00.000Z", + "updated_at": "2025-02-08T12:00:00.000Z" +} \ No newline at end of file diff --git a/claude/__init__.py b/claude/__init__.py deleted file mode 100644 index a1de208..0000000 --- a/claude/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# backend/__init__.py diff --git a/claude/__init___1.py b/claude/__init___1.py deleted file mode 100644 index d258405..0000000 --- a/claude/__init___1.py +++ /dev/null @@ -1 +0,0 @@ -# backend/core/__init__.py diff --git a/claude/__init___2.py b/claude/__init___2.py deleted file mode 100644 index 772a730..0000000 --- a/claude/__init___2.py +++ /dev/null @@ -1 +0,0 @@ -# backend/script_groups/__init__.py diff --git a/claude/__init___3.py b/claude/__init___3.py deleted file mode 100644 index 4e7ed26..0000000 --- a/claude/__init___3.py +++ /dev/null @@ -1 +0,0 @@ -# backend/script_groups/example_group/__init__.py diff --git a/claude/app.py b/claude/app.py deleted file mode 100644 index 1bf0035..0000000 --- a/claude/app.py +++ /dev/null @@ -1,193 +0,0 @@ -# backend/app.py -import os -import sys -from pathlib import Path - -# Add the parent directory to Python path -backend_dir = Path(__file__).parent.parent # Sube un nivel más para incluir la carpeta raíz -if str(backend_dir) not in sys.path: - sys.path.append(str(backend_dir)) - -from flask import Flask, render_template, jsonify, request, send_from_directory -from core.directory_handler import select_directory -from core.script_manager import ScriptManager -from core.profile_manager import ProfileManager - -app = Flask(__name__, - template_folder='../frontend/templates', - static_folder='../frontend/static') - -# Initialize managers -data_dir = Path(__file__).parent.parent / 'data' -script_groups_dir = Path(__file__).parent / 'script_groups' - -profile_manager = ProfileManager(data_dir) -script_manager = ScriptManager(script_groups_dir) - -@app.route('/') -def index(): - """Render main page""" - return render_template('index.html') - -# Profile endpoints -@app.route('/api/profiles', methods=['GET']) -def get_profiles(): - """Get all profiles""" - return jsonify(profile_manager.get_all_profiles()) - -@app.route('/api/profiles/<profile_id>', methods=['GET']) -def get_profile(profile_id): - """Get specific profile""" - profile = profile_manager.get_profile(profile_id) - if profile: - return jsonify(profile) - return jsonify({"error": "Profile not found"}), 404 - -@app.route('/api/profiles', methods=['POST']) -def create_profile(): - """Create new profile""" - profile_data = request.json - try: - profile = profile_manager.create_profile(profile_data) - return jsonify(profile) - except Exception as e: - return jsonify({"error": str(e)}), 400 - -@app.route('/api/profiles/<profile_id>', methods=['PUT']) -def update_profile(profile_id): - """Update existing profile""" - try: - profile_data = request.json - print(f"Received update request for profile {profile_id}: {profile_data}") # Debug - profile = profile_manager.update_profile(profile_id, profile_data) - print(f"Profile updated: {profile}") # Debug - return jsonify(profile) - except Exception as e: - print(f"Error updating profile: {e}") # Debug - return jsonify({"error": str(e)}), 400 - -@app.route('/api/profiles/<profile_id>', methods=['DELETE']) -def delete_profile(profile_id): - """Delete profile""" - try: - profile_manager.delete_profile(profile_id) - return jsonify({"status": "success"}) - except Exception as e: - return jsonify({"error": str(e)}), 400 - -@app.route('/api/script-groups', methods=['GET']) -def get_script_groups(): - """Get all available script groups""" - try: - groups = script_manager.get_available_groups() - return jsonify(groups) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# Directory handling endpoints -@app.route('/api/select-directory', methods=['GET']) -def handle_select_directory(): - """Handle directory selection""" - print("Handling directory selection request") # Debug - result = select_directory() - print(f"Directory selection result: {result}") # Debug - if "error" in result: - return jsonify(result), 400 - return jsonify(result) - -# Script management endpoints -@app.route('/api/scripts', methods=['GET']) -def get_scripts(): - """Get all available script groups""" - try: - groups = script_manager.discover_groups() - return jsonify(groups) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -@app.route('/api/scripts/<group_id>/<script_id>/run', methods=['POST']) -def run_script(group_id, script_id): - """Execute a specific script""" - data = request.json - work_dir = data.get('work_dir') - profile = data.get('profile') - - if not work_dir: - return jsonify({"error": "Work directory not specified"}), 400 - if not profile: - return jsonify({"error": "Profile not specified"}), 400 - - try: - result = script_manager.execute_script(group_id, script_id, work_dir, profile) - return jsonify(result) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -# Work directory configuration endpoints -@app.route('/api/workdir-config/<path:work_dir>', methods=['GET']) -def get_workdir_config(work_dir): - """Get work directory configuration""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - return jsonify(config_manager.get_config()) - -@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['GET']) -def get_group_config(work_dir, group_id): - """Get group configuration from work directory""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - return jsonify(config_manager.get_group_config(group_id)) - -@app.route('/api/workdir-config/<path:work_dir>/group/<group_id>', methods=['PUT']) -def update_group_config(work_dir, group_id): - """Update group configuration in work directory""" - from core.workdir_config import WorkDirConfigManager - config_manager = WorkDirConfigManager(work_dir) - - try: - settings = request.json - config_manager.update_group_config(group_id, settings) - return jsonify({"status": "success"}) - except Exception as e: - return jsonify({"error": str(e)}), 400 - -@app.route('/api/script-groups/<group_id>/config-schema', methods=['PUT']) -def update_group_config_schema(group_id): - """Update configuration schema for a script group""" - try: - schema = request.json - config_file = Path(script_manager.script_groups_dir) / group_id / "config.json" - - with open(config_file, 'w', encoding='utf-8') as f: - json.dump(schema, f, indent=4) - - return jsonify({"status": "success"}) - except Exception as e: - return jsonify({"error": str(e)}), 500 - -@app.route('/api/script-groups/<group_id>/scripts', methods=['GET']) -def get_group_scripts(group_id): - """Get scripts for a specific group""" - try: - print(f"Loading scripts for group: {group_id}") # Debug - scripts = script_manager.get_group_scripts(group_id) - print(f"Scripts found: {scripts}") # Debug - return jsonify(scripts) - except Exception as e: - print(f"Error loading scripts: {str(e)}") # Debug - return jsonify({"error": str(e)}), 500 - -@app.route('/api/script-groups/<group_id>/config-schema', methods=['GET']) -def get_group_config_schema(group_id): - """Get configuration schema for a script group""" - try: - print(f"Loading config schema for group: {group_id}") # Debug - schema = script_manager.get_group_config_schema(group_id) - print(f"Schema loaded: {schema}") # Debug - return jsonify(schema) - except Exception as e: - print(f"Error loading schema: {str(e)}") # Debug - return jsonify({"error": str(e)}), 500 - -if __name__ == '__main__': - app.run(debug=True, port=5000) diff --git a/claude/base.html b/claude/base.html deleted file mode 100644 index 4282250..0000000 --- a/claude/base.html +++ /dev/null @@ -1,15 +0,0 @@ -<!DOCTYPE html> -<!-- frontend/templates/base.html --> - -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Local Scripts Web</title> - <script src="https://cdn.tailwindcss.com"></script> - <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> -</head> -<body> - {% block content %}{% endblock %} -</body> -</html> \ No newline at end of file diff --git a/claude/base_script.py b/claude/base_script.py deleted file mode 100644 index 2a34b9a..0000000 --- a/claude/base_script.py +++ /dev/null @@ -1,61 +0,0 @@ -# backend/script_groups/base_script.py -from typing import Dict, Any -from pathlib import Path -import json - -class BaseScript: - """Base class for all scripts""" - - def run(self, work_dir: str, profile: Dict[str, Any]) -> Dict[str, Any]: - """ - Execute the script - - Args: - work_dir (str): Working directory path - profile (Dict[str, Any]): Current profile configuration - - Returns: - Dict[str, Any]: Execution results - """ - raise NotImplementedError("Script must implement run method") - - def get_config(self, work_dir: str, group_id: str) -> Dict[str, Any]: - """Get group configuration from work directory""" - config_file = Path(work_dir) / "script_config.json" - - if config_file.exists(): - try: - with open(config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - return config.get("group_settings", {}).get(group_id, {}) - except Exception as e: - print(f"Error loading config: {e}") - - return {} - - def save_config(self, work_dir: str, group_id: str, settings: Dict[str, Any]): - """Save group configuration to work directory""" - config_file = Path(work_dir) / "script_config.json" - - try: - # Load existing config or create new - if config_file.exists(): - with open(config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - else: - config = { - "version": "1.0", - "group_settings": {} - } - - # Update settings - if "group_settings" not in config: - config["group_settings"] = {} - config["group_settings"][group_id] = settings - - # Save config - with open(config_file, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=4) - - except Exception as e: - print(f"Error saving config: {e}") diff --git a/claude/claude_file_organizer.py b/claude/claude_file_organizer.py deleted file mode 100644 index 93391ad..0000000 --- a/claude/claude_file_organizer.py +++ /dev/null @@ -1,172 +0,0 @@ -# claude_file_organizer.py -import os -import shutil -from pathlib import Path -import re - -class ClaudeProjectOrganizer: - def __init__(self): - self.source_dir = Path.cwd() - self.claude_dir = self.source_dir / 'claude' - self.file_mapping = {} - - def should_skip_directory(self, dir_name): - skip_dirs = {'.git', '__pycache__', 'venv', 'env', '.pytest_cache', '.vscode', 'claude'} - return dir_name in skip_dirs - - def get_comment_prefix(self, file_extension): - """Determina el prefijo de comentario según la extensión del archivo""" - comment_styles = { - '.py': '#', - '.js': '//', - '.css': '/*', - '.html': '<!--', - '.scss': '//', - '.less': '//', - '.tsx': '//', - '.ts': '//', - '.jsx': '//', - } - return comment_styles.get(file_extension.lower(), None) - - def get_comment_suffix(self, file_extension): - """Determina el sufijo de comentario si es necesario""" - comment_suffixes = { - '.css': ' */', - '.html': ' -->', - } - return comment_suffixes.get(file_extension.lower(), '') - - def normalize_path(self, path_str: str) -> str: - """Normaliza la ruta usando forward slashes""" - return str(path_str).replace('\\', '/') - - def check_existing_path_comment(self, content: str, normalized_path: str, comment_prefix: str) -> bool: - """Verifica si ya existe un comentario con la ruta en el archivo""" - # Escapar caracteres especiales en el prefijo de comentario para regex - escaped_prefix = re.escape(comment_prefix) - - # Crear patrones para buscar tanto forward como backward slashes - forward_pattern = f"{escaped_prefix}\\s*{re.escape(normalized_path)}\\b" - backward_path = normalized_path.replace('/', '\\\\') # Doble backslash para el patrón - backward_pattern = f"{escaped_prefix}\\s*{re.escape(backward_path)}" - - # Buscar en las primeras líneas del archivo - first_lines = content.split('\n')[:5] - for line in first_lines: - if (re.search(forward_pattern, line) or - re.search(backward_pattern, line)): - return True - return False - - def add_path_comment(self, file_path: Path, content: str) -> str: - """Agrega un comentario con la ruta al inicio del archivo si no existe""" - relative_path = file_path.relative_to(self.source_dir) - normalized_path = self.normalize_path(relative_path) - comment_prefix = self.get_comment_prefix(file_path.suffix) - - if comment_prefix is None: - return content - - comment_suffix = self.get_comment_suffix(file_path.suffix) - - # Verificar si ya existe el comentario - if self.check_existing_path_comment(content, normalized_path, comment_prefix): - print(f" - Comentario de ruta ya existe en {file_path}") - return content - - path_comment = f"{comment_prefix} {normalized_path}{comment_suffix}\n" - - # Para archivos HTML, insertar después del doctype si existe - if file_path.suffix.lower() == '.html': - if content.lower().startswith('<!doctype'): - doctype_end = content.find('>') + 1 - return content[:doctype_end] + '\n' + path_comment + content[doctype_end:] - - return path_comment + content - - def clean_claude_directory(self): - if self.claude_dir.exists(): - shutil.rmtree(self.claude_dir) - self.claude_dir.mkdir() - print(f"Directorio claude limpiado: {self.claude_dir}") - - def copy_files(self): - self.clean_claude_directory() - - for root, dirs, files in os.walk(self.source_dir): - dirs[:] = [d for d in dirs if not self.should_skip_directory(d)] - current_path = Path(root) - - for file in files: - file_path = current_path / file - - if file.endswith(('.py', '.js', '.css', '.html', '.json', '.yml', '.yaml', - '.tsx', '.ts', '.jsx', '.scss', '.less')): - target_path = self.claude_dir / file - - # Si el archivo ya existe en el directorio claude, agregar un sufijo numérico - if target_path.exists(): - base = target_path.stem - ext = target_path.suffix - counter = 1 - while target_path.exists(): - target_path = self.claude_dir / f"{base}_{counter}{ext}" - counter += 1 - - try: - # Leer el contenido del archivo - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Agregar el comentario con la ruta si no existe - modified_content = self.add_path_comment(file_path, content) - - # Escribir el nuevo contenido - with open(target_path, 'w', encoding='utf-8', newline='\n') as f: - f.write(modified_content) - - self.file_mapping[str(file_path)] = target_path.name - print(f"Copiado: {file_path} -> {target_path}") - - except UnicodeDecodeError: - print(f"Advertencia: No se pudo procesar {file_path} como texto. Copiando sin modificar...") - shutil.copy2(file_path, target_path) - except Exception as e: - print(f"Error procesando {file_path}: {str(e)}") - - def generate_tree_report(self): - """Genera el reporte en formato árbol visual""" - report = ["Estructura del proyecto original:\n"] - - def add_to_report(path, prefix="", is_last=True): - report.append(prefix + ("└── " if is_last else "├── ") + path.name) - - if path.is_dir() and not self.should_skip_directory(path.name): - children = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name)) - children = [c for c in children if not (c.is_dir() and self.should_skip_directory(c.name))] - - for i, child in enumerate(children): - is_last_child = i == len(children) - 1 - new_prefix = prefix + (" " if is_last else "│ ") - add_to_report(child, new_prefix, is_last_child) - - add_to_report(self.source_dir) - - report_path = self.claude_dir / "project_structure.txt" - with open(report_path, "w", encoding="utf-8") as f: - f.write("\n".join(report)) - print(f"\nReporte generado en: {report_path}") - -def main(): - try: - print("Iniciando organización de archivos para Claude...") - organizer = ClaudeProjectOrganizer() - organizer.copy_files() - organizer.generate_tree_report() - print("\n¡Proceso completado exitosamente!") - except Exception as e: - print(f"\nError durante la ejecución: {str(e)}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/claude/config.json b/claude/config.json deleted file mode 100644 index 4886062..0000000 --- a/claude/config.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "description": "Configuration schema for script groups", - "config_schema": { - "work_dir": { - "type": "string", - "description": "Working directory for this script group", - "required": true - }, - "description": { - "type": "string", - "description": "Description of this script group", - "default": "" - }, - "backup_dir": { - "type": "directory", - "description": "Backup directory path", - "default": "" - }, - "max_files": { - "type": "number", - "description": "Maximum number of files to process", - "default": 1000 - }, - "enable_backup": { - "type": "boolean", - "description": "Enable automatic backups", - "default": false - } - } -} diff --git a/claude/config_1.json b/claude/config_1.json deleted file mode 100644 index 36be0b3..0000000 --- a/claude/config_1.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "group_name": "System Analysis", - "description": "Scripts for system analysis and file management", - "config_schema": { - "exclude_dirs": { - "type": "string", - "description": "Directories to exclude (comma separated)", - "default": "venv,__pycache__,.git" - }, - "count_hidden": { - "type": "boolean", - "description": "Include hidden files in count", - "default": false - }, - "min_size": { - "type": "number", - "description": "Minimum file size to count (in bytes)", - "default": 0 - }, - "save_report": { - "type": "boolean", - "description": "Save results to file", - "default": true - }, - "report_format": { - "type": "select", - "options": ["txt", "json", "csv"], - "description": "Format for saved reports", - "default": "json" - } - } -} \ No newline at end of file diff --git a/claude/directory_handler.py b/claude/directory_handler.py deleted file mode 100644 index 53f5ad9..0000000 --- a/claude/directory_handler.py +++ /dev/null @@ -1,23 +0,0 @@ -# backend/core/directory_handler.py -import os -from pathlib import Path -import tkinter as tk -from tkinter import filedialog -from flask import jsonify - -def select_directory(): - """Show directory selection dialog and return selected path""" - root = tk.Tk() - root.withdraw() - root.attributes('-topmost', True) # Hace que el diálogo siempre esté encima - - try: - directory = filedialog.askdirectory( - title="Select Work Directory", - initialdir=os.path.expanduser("~") - ) - return {"path": directory} if directory else {"error": "No directory selected"} - except Exception as e: - return {"error": str(e)} - finally: - root.destroy() \ No newline at end of file diff --git a/claude/group_settings_manager.py b/claude/group_settings_manager.py deleted file mode 100644 index 4e04cc8..0000000 --- a/claude/group_settings_manager.py +++ /dev/null @@ -1,122 +0,0 @@ -# backend/core/group_settings_manager.py -from pathlib import Path -import json -from typing import Dict, Any -from datetime import datetime -import os - - -class GroupSettingsManager: - """Manages settings for script groups""" - - def __init__(self, script_groups_dir: Path): - self.script_groups_dir = script_groups_dir - self.config_schema = self._load_config_schema() - - def _load_config_schema(self) -> Dict[str, Any]: - """Load the main configuration schema for script groups""" - schema_file = self.script_groups_dir / "config.json" - try: - with open(schema_file, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - print(f"Error loading group config schema: {e}") - return { - "config_schema": { - "work_dir": { - "type": "string", - "description": "Working directory for this script group", - "required": True, - }, - "description": { - "type": "string", - "description": "Description of this script group", - "default": "", - }, - } - } - - def _validate_setting( - self, key: str, value: Any, field_schema: Dict[str, Any] - ) -> Any: - """Validate and convert a single setting value""" - field_type = field_schema.get("type") - - if value is None or value == "": - if field_schema.get("required", False): - raise ValueError(f"Field '{key}' is required") - return field_schema.get("default") - - try: - if field_type == "string": - return str(value) - elif field_type == "number": - return float(value) if "." in str(value) else int(value) - elif field_type == "boolean": - if isinstance(value, str): - return value.lower() == "true" - return bool(value) - elif field_type == "directory": - path = Path(value) - if not path.is_absolute(): - path = Path(self.script_groups_dir) / path - if not path.exists(): - path.mkdir(parents=True, exist_ok=True) - return str(path) - else: - return value - except Exception as e: - raise ValueError(f"Invalid value for field '{key}': {str(e)}") - - def get_group_settings(self, group_id: str) -> Dict[str, Any]: - """Get settings for a specific script group""" - settings_file = self.script_groups_dir / group_id / "group.json" - - if settings_file.exists(): - try: - with open(settings_file, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - print(f"Error loading group settings: {e}") - - return { - "work_dir": "", - "description": "", - "created_at": datetime.now().isoformat(), - "updated_at": datetime.now().isoformat(), - } - - def update_group_settings(self, group_id: str, settings: Dict[str, Any]): - """Update settings for a specific script group""" - schema = self.config_schema.get("config_schema", {}) - validated_settings = {} - - # Validate each setting against schema - for key, field_schema in schema.items(): - if key in settings: - validated_settings[key] = self._validate_setting( - key, settings[key], field_schema - ) - elif field_schema.get("required", False): - raise ValueError(f"Required field '{key}' is missing") - else: - validated_settings[key] = field_schema.get("default") - - # Add non-schema fields - for key, value in settings.items(): - if key not in schema: - validated_settings[key] = value - - # Update timestamps - validated_settings["updated_at"] = datetime.now().isoformat() - - group_dir = self.script_groups_dir / group_id - settings_file = group_dir / "group.json" - - if not settings_file.exists(): - validated_settings["created_at"] = validated_settings["updated_at"] - group_dir.mkdir(parents=True, exist_ok=True) - - # Save settings - with open(settings_file, "w", encoding="utf-8") as f: - json.dump(validated_settings, f, indent=4) diff --git a/claude/index.html b/claude/index.html deleted file mode 100644 index 0912f1a..0000000 --- a/claude/index.html +++ /dev/null @@ -1,115 +0,0 @@ -<!DOCTYPE html> -<!-- frontend/templates/index.html --> - -<html lang="en" class="h-full bg-gray-50"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Local Scripts Web</title> - <!-- Tailwind y Alpine.js desde CDN --> - <script src="https://cdn.tailwindcss.com"></script> - <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> - <!-- HeroIcons --> - <script src="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.0.18/solid/index.min.js"></script> -</head> -<body class="h-full"> - <div class="min-h-full"> - <!-- Navbar --> - <nav class="bg-white shadow-sm"> - <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> - <div class="flex h-16 justify-between"> - <div class="flex"> - <div class="flex flex-shrink-0 items-center"> - <h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1> - </div> - </div> - <div class="flex items-center gap-4"> - <select id="profileSelect" - class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600" - onchange="changeProfile()"> - <option value="">Select Profile</option> - </select> - <button onclick="editProfile()" - class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Edit Profile - </button> - <button onclick="newProfile()" - class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - New Profile - </button> - </div> - </div> - </div> - </nav> - - <!-- Main content --> - <main> - <div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> - <div class="space-y-6"> - <!-- Work Directory Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Work Directory</h3> - <div class="mt-4 flex gap-4"> - <input type="text" id="workDirPath" readonly - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <button onclick="selectWorkDir()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Browse - </button> - </div> - </div> - </div> - - <!-- Scripts Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3> - <div class="mt-4 space-y-4"> - <select id="groupSelect" - class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <option value="">Select Script Group</option> - </select> - <div id="scriptList" class="hidden space-y-4"> - <!-- Scripts will be loaded here --> - </div> - </div> - </div> - </div> - - <!-- Output Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <div class="flex justify-between items-center"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3> - <button onclick="clearOutput()" - class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500"> - Clear - </button> - </div> - <div id="outputArea" - class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200"> - </div> - </div> - </div> - </div> - </div> - </main> - </div> - - <!-- Scripts --> - <script src="{{ url_for('static', filename='js/main.js') }}"></script> - <script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script> - <script src="{{ url_for('static', filename='js/profile.js') }}"></script> - <script src="{{ url_for('static', filename='js/scripts.js') }}"></script> - <script src="{{ url_for('static', filename='js/modal.js') }}"></script> - <!-- Al final del body --> - <script> - // Initialización cuando la página carga - document.addEventListener('DOMContentLoaded', async () => { - console.log('DOM loaded, initializing...'); - await initializeApp(); - }); - </script> -</body> -</html> \ No newline at end of file diff --git a/claude/main.js b/claude/main.js deleted file mode 100644 index 951ab99..0000000 --- a/claude/main.js +++ /dev/null @@ -1,264 +0,0 @@ -// frontend/static/js/main.js - -// Global state -let currentProfile = null; - -// Definir clases comunes para inputs -const STYLES = { - editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500", - readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm", - button: "px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600", - buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300" -}; - -async function initializeApp() { - try { - console.log('Inicializando aplicación...'); - - // Cargar perfiles - const profiles = await apiRequest('/profiles'); - console.log('Profiles loaded:', profiles); - - // Obtener último perfil usado - const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; - console.log('Last profile ID:', lastProfileId); - - // Actualizar selector de perfiles - updateProfileSelector(profiles); - - // Seleccionar el último perfil usado - const selectedProfile = profiles.find(p => p.id === lastProfileId) || profiles[0]; - if (selectedProfile) { - console.log('Selecting profile:', selectedProfile.id); - await selectProfile(selectedProfile.id); - } - - // Cargar grupos de scripts y restaurar la última selección - await restoreScriptGroup(); - - // Actualizar la interfaz - updateWorkDirDisplay(); - - } catch (error) { - console.error('Error al inicializar la aplicación:', error); - showError('Error al inicializar la aplicación'); - } -} - -async function restoreScriptGroup() { - try { - // Primero cargar los grupos disponibles - await loadScriptGroups(); - - // Luego intentar restaurar el último grupo seleccionado - const lastGroupId = localStorage.getItem('lastGroupId'); - if (lastGroupId) { - console.log('Restoring last group:', lastGroupId); - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - if (groupSelect.value) { // Verifica que el valor se haya establecido correctamente - await loadGroupScripts(lastGroupId); - } else { - console.log('Selected group no longer exists:', lastGroupId); - localStorage.removeItem('lastGroupId'); - } - } - } - } catch (error) { - console.error('Error restoring script group:', error); - } -} - -// Función para restaurar el último estado -async function restoreLastState() { - const lastProfileId = localStorage.getItem('lastProfileId'); - const lastGroupId = localStorage.getItem('lastGroupId'); - - console.log('Restoring last state:', { lastProfileId, lastGroupId }); - - if (lastProfileId) { - const profileSelect = document.getElementById('profileSelect'); - profileSelect.value = lastProfileId; - await selectProfile(lastProfileId); - } - - if (lastGroupId) { - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - await loadGroupScripts(lastGroupId); - } - } -} - -// API functions -async function apiRequest(endpoint, options = {}) { - try { - const response = await fetch(`/api${endpoint}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Error en la solicitud API'); - } - - return await response.json(); - } catch (error) { - console.error('Error API:', error); - showError(error.message); - throw error; - } -} - -async function loadProfiles() { - try { - const profiles = await apiRequest('/profiles'); - updateProfileSelector(profiles); - - // Obtener último perfil usado - const lastProfileId = localStorage.getItem('lastProfileId'); - - // Seleccionar perfil guardado o el default - const defaultProfile = profiles.find(p => p.id === (lastProfileId || 'default')) || profiles[0]; - if (defaultProfile) { - await selectProfile(defaultProfile.id); - } - } catch (error) { - showError('Error al cargar los perfiles'); - } -} - -async function selectProfile(profileId) { - try { - console.log('Seleccionando perfil:', profileId); - currentProfile = await apiRequest(`/profiles/${profileId}`); - - // Guardar en localStorage - localStorage.setItem('lastProfileId', profileId); - console.log('Profile ID saved to storage:', profileId); - - // Actualizar explícitamente el valor del combo - const select = document.getElementById('profileSelect'); - if (select) { - select.value = profileId; - console.log('Updated profileSelect value to:', profileId); - } - - updateWorkDirDisplay(); - - // Recargar scripts con el último grupo seleccionado - await restoreScriptGroup(); - } catch (error) { - console.error('Error al seleccionar perfil:', error); - showError('Error al cargar el perfil'); - } -} - -// Initialize when page loads -document.addEventListener('DOMContentLoaded', initializeApp); - -function updateProfileSelector(profiles) { - const select = document.getElementById('profileSelect'); - const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; - - console.log('Updating profile selector. Last profile ID:', lastProfileId); - - // Construir las opciones - select.innerHTML = profiles.map(profile => ` - <option value="${profile.id}" ${profile.id === lastProfileId ? 'selected' : ''}> - ${profile.name} - </option> - `).join(''); - - // Asegurar que el valor seleccionado sea correcto - select.value = lastProfileId; - console.log('Set profileSelect value to:', lastProfileId); -} - -async function changeProfile() { - const select = document.getElementById('profileSelect'); - if (select.value) { - await selectProfile(select.value); - await loadScriptGroups(); // Reload scripts when profile changes - } -} - -// Work directory functions -function updateWorkDirDisplay() { - const input = document.getElementById('workDirPath'); - if (input && currentProfile) { - input.value = currentProfile.work_dir || ''; - } -} - -async function selectWorkDir() { - try { - console.log('Requesting directory selection...'); // Debug - const response = await apiRequest('/select-directory'); - console.log('Directory selection response:', response); // Debug - - if (response.path) { - console.log('Updating profile with new work_dir:', response.path); // Debug - const updateResponse = await apiRequest(`/profiles/${currentProfile.id}`, { - method: 'PUT', - body: JSON.stringify({ - ...currentProfile, - work_dir: response.path - }) - }); - console.log('Profile update response:', updateResponse); // Debug - - await selectProfile(currentProfile.id); - showSuccess('Directorio de trabajo actualizado correctamente'); - } - } catch (error) { - console.error('Error al seleccionar directorio:', error); // Debug - showError('Error al actualizar el directorio de trabajo'); - } -} - -// Output functions -function showError(message) { - const output = document.getElementById('outputArea'); - const timestamp = new Date().toLocaleTimeString(); - output.innerHTML += `\n[${timestamp}] ERROR: ${message}`; - output.scrollTop = output.scrollHeight; -} - -function showSuccess(message) { - const output = document.getElementById('outputArea'); - const timestamp = new Date().toLocaleTimeString(); - output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`; - output.scrollTop = output.scrollHeight; -} - -function clearOutput() { - const output = document.getElementById('outputArea'); - output.innerHTML = ''; -} - -// Modal helper functions -function closeModal(button) { - const modal = button.closest('.modal'); - if (modal) { - modal.remove(); - } -} - -// Global error handler -window.addEventListener('unhandledrejection', function(event) { - console.error('Unhandled promise rejection:', event.reason); - showError('An unexpected error occurred'); -}); - -// Export functions for use in other modules -window.showError = showError; -window.showSuccess = showSuccess; -window.closeModal = closeModal; -window.currentProfile = currentProfile; \ No newline at end of file diff --git a/claude/modal.js b/claude/modal.js deleted file mode 100644 index 4e17e74..0000000 --- a/claude/modal.js +++ /dev/null @@ -1,39 +0,0 @@ -// frontend/static/js/modal.js -// static/js/modal.js -function createModal(title, content, onSave = null) { - const modal = document.createElement('div'); - modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50'; - - modal.innerHTML = ` - <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4"> - <div class="px-6 py-4 border-b border-gray-200"> - <h3 class="text-lg font-medium text-gray-900">${title}</h3> - </div> - <div class="px-6 py-4"> - ${content} - </div> - <div class="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end gap-3"> - <button onclick="closeModal(this)" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cancel - </button> - ${onSave ? ` - <button onclick="saveModal(this)" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Save - </button> - ` : ''} - </div> - </div> - `; - - document.body.appendChild(modal); - return modal; -} - -function closeModal(button) { - const modal = button.closest('.fixed'); - if (modal) { - modal.remove(); - } -} \ No newline at end of file diff --git a/claude/profile.js b/claude/profile.js deleted file mode 100644 index 4904f92..0000000 --- a/claude/profile.js +++ /dev/null @@ -1,324 +0,0 @@ -// frontend/static/js/profile.js -let selectedProfileId = localStorage.getItem('selectedProfileId') || 'default'; -let editingProfile = null; - -// Profile functions -async function loadProfiles() { - try { - const response = await apiRequest('/profiles'); - const profiles = Object.values(response); - - // Actualizar el selector manteniendo el valor seleccionado - const select = document.getElementById('profileSelect'); - select.innerHTML = profiles.map(profile => ` - <option value="${profile.id}"> - ${profile.name} - </option> - `).join(''); - - // Establecer el valor seleccionado después de actualizar las opciones - if (response[selectedProfileId]) { - select.value = selectedProfileId; - await selectProfile(selectedProfileId); - } else { - selectedProfileId = 'default'; - select.value = 'default'; - await selectProfile('default'); - } - - // Asegurarse de que el evento change no sobrescriba la selección - select.addEventListener('change', onProfileChange, { once: true }); - - } catch (error) { - showError('Error al cargar los perfiles'); - } -} - -async function selectProfile(profileId) { - try { - currentProfile = await apiRequest(`/profiles/${profileId}`); - updateWorkDirDisplay(); - } catch (error) { - showError('Failed to load profile'); - } -} - -async function changeProfile() { - const select = document.getElementById('profileSelect'); - await selectProfile(select.value); -} - -async function selectWorkDir() { - try { - const response = await apiRequest('/select-directory'); - if (response.path) { - await apiRequest(`/profiles/${currentProfile.id}`, { - method: 'PUT', - body: JSON.stringify({ - ...currentProfile, - work_dir: response.path - }) - }); - await selectProfile(currentProfile.id); - showSuccess('Work directory updated successfully'); - } - } catch (error) { - showError('Failed to update work directory'); - } -} - -// Profile editor modal - -function showProfileEditor(profile = null) { - editingProfile = profile; - const modal = document.createElement('div'); - modal.className = 'modal active'; - - const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"; - const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm"; - - modal.innerHTML = ` - <div class="modal-content"> - <h2 class="text-xl font-bold mb-4">${profile ? 'Editar Perfil' : 'Nuevo Perfil'}</h2> - <form id="profileForm" onsubmit="saveProfile(event)"> - <div class="form-group"> - <label for="profileId" class="block text-sm font-medium text-gray-700">ID del Perfil</label> - <input type="text" id="profileId" name="id" - class="${profile ? readonlyInputClass : editableInputClass}" - value="${profile?.id || ''}" - ${profile ? 'readonly' : ''} - required pattern="[a-zA-Z0-9_-]+" - title="Solo se permiten letras, números, guión bajo y guión"> - </div> - <div class="form-group"> - <label for="profileName" class="block text-sm font-medium text-gray-700">Nombre</label> - <input type="text" id="profileName" name="name" - class="${editableInputClass}" - value="${profile?.name || ''}" required> - </div> - <div class="form-group"> - <label for="workDir" class="block text-sm font-medium text-gray-700">Directorio de Trabajo</label> - <input type="text" id="workDir" name="work_dir" - class="${readonlyInputClass}" - value="${profile?.work_dir || ''}" readonly> - </div> - <div class="form-group"> - <label for="llmModel" class="block text-sm font-medium text-gray-700">LLM Model</label> - <select id="llmModel" name="llm_model" - class="${editableInputClass}"> - <option value="gpt-4" ${profile?.llm_settings?.model === 'gpt-4' ? 'selected' : ''}>GPT-4</option> - <option value="gpt-3.5-turbo" ${profile?.llm_settings?.model === 'gpt-3.5-turbo' ? 'selected' : ''}>GPT-3.5 Turbo</option> - </select> - </div> - <div class="form-group"> - <label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label> - <input type="password" id="apiKey" name="api_key" - class="${editableInputClass}" - value="${profile?.llm_settings?.api_key || ''}"> - </div> - <div class="form-group"> - <label for="temperature" class="block text-sm font-medium text-gray-700">Temperature</label> - <input type="number" id="temperature" name="temperature" - class="${editableInputClass}" - value="${profile?.llm_settings?.temperature || 0.7}" - min="0" max="2" step="0.1"> - </div> - <div class="mt-4 flex justify-end space-x-3"> - <button type="button" onclick="closeModal(this)" - class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancelar</button> - <button type="submit" - class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Guardar</button> - </div> - </form> - </div> - `; - - document.body.appendChild(modal); -} - -async function saveProfile(event) { - event.preventDefault(); - const form = event.target; - const formData = new FormData(form); - - const profileData = { - id: formData.get('id'), - name: formData.get('name'), - work_dir: formData.get('work_dir'), - llm_settings: { - model: formData.get('llm_model'), - api_key: formData.get('api_key'), - temperature: parseFloat(formData.get('temperature')) - } - }; - - try { - if (editingProfile) { - await apiRequest(`/profiles/${editingProfile.id}`, { - method: 'PUT', - body: JSON.stringify(profileData) - }); - } else { - await apiRequest('/profiles', { - method: 'POST', - body: JSON.stringify(profileData) - }); - } - - await loadProfiles(); - closeModal(event.target); - showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`); - } catch (error) { - showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`); - } -} - -// static/js/profile.js -async function editProfile() { - if (!currentProfile) { - showError('No profile selected'); - return; - } - - const content = ` - <form id="profileForm" class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Profile ID</label> - <input type="text" name="id" value="${currentProfile.id}" - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" - ${currentProfile.id === 'default' ? 'readonly' : ''}> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Name</label> - <input type="text" name="name" value="${currentProfile.name}" - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Work Directory</label> - <input type="text" name="work_dir" value="${currentProfile.work_dir}" readonly - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">LLM Model</label> - <select name="llm_model" - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> - <option value="gpt-4" ${currentProfile.llm_settings?.model === 'gpt-4' ? 'selected' : ''}>GPT-4</option> - <option value="gpt-3.5-turbo" ${currentProfile.llm_settings?.model === 'gpt-3.5-turbo' ? 'selected' : ''}>GPT-3.5 Turbo</option> - </select> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">API Key</label> - <input type="password" name="api_key" value="${currentProfile.llm_settings?.api_key || ''}" - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Temperature</label> - <input type="number" name="temperature" value="${currentProfile.llm_settings?.temperature || 0.7}" - min="0" max="2" step="0.1" - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> - </div> - </form> - `; - - const modal = createModal('Edit Profile', content, true); - modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => { - await saveProfile(modal); - }; -} - -async function saveProfile(modal) { - const form = modal.querySelector('#profileForm'); - const formData = new FormData(form); - - const profileData = { - id: formData.get('id'), - name: formData.get('name'), - work_dir: formData.get('work_dir'), - llm_settings: { - model: formData.get('llm_model'), - api_key: formData.get('api_key'), - temperature: parseFloat(formData.get('temperature')) - } - }; - - try { - if (editingProfile) { - await apiRequest(`/profiles/${editingProfile.id}`, { - method: 'PUT', - body: JSON.stringify(profileData) - }); - } else { - await apiRequest('/profiles', { - method: 'POST', - body: JSON.stringify(profileData) - }); - } - - await loadProfiles(); - closeModal(modal); - showSuccess(`Perfil ${editingProfile ? 'actualizado' : 'creado'} correctamente`); - } catch (error) { - showError(`Error al ${editingProfile ? 'actualizar' : 'crear'} el perfil`); - } -} - -function newProfile() { - const editableInputClass = "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"; - const readonlyInputClass = "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm"; - - const content = ` - <form id="profileForm" class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Profile ID</label> - <input type="text" name="id" required pattern="[a-zA-Z0-9_-]+" - class="${editableInputClass}" - title="Only letters, numbers, underscore and dash allowed"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Name</label> - <input type="text" name="name" required - class="${editableInputClass}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Work Directory</label> - <input type="text" name="work_dir" readonly - class="${readonlyInputClass}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">LLM Model</label> - <select name="llm_model" - class="${editableInputClass}"> - <option value="gpt-4">GPT-4</option> - <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option> - </select> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">API Key</label> - <input type="password" name="api_key" - class="${editableInputClass}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Temperature</label> - <input type="number" name="temperature" value="0.7" - min="0" max="2" step="0.1" - class="${editableInputClass}"> - </div> - </form> - `; - - const modal = createModal('New Profile', content, true); - editingProfile = null; - - modal.querySelector('[onclick="saveModal(this)"]').onclick = async () => { - await saveProfile(modal); - }; -} - -async function onProfileChange(event) { - const newProfileId = event.target.value; - if (newProfileId !== selectedProfileId) { - selectedProfileId = newProfileId; - localStorage.setItem('selectedProfileId', selectedProfileId); - await selectProfile(selectedProfileId); - } -} \ No newline at end of file diff --git a/claude/profile_manager.py b/claude/profile_manager.py deleted file mode 100644 index 3baa562..0000000 --- a/claude/profile_manager.py +++ /dev/null @@ -1,133 +0,0 @@ -# backend/core/profile_manager.py -from pathlib import Path -import json -from typing import Dict, Any, List, Optional -from datetime import datetime - - -class ProfileManager: - """Manages configuration profiles""" - - DEFAULT_PROFILE = { - "id": "default", - "name": "Default Profile", - "llm_settings": {"model": "gpt-4", "temperature": 0.7, "api_key": ""}, - "created_at": "", - "updated_at": "", - } - - def __init__(self, data_dir: Path): - self.data_dir = data_dir - self.profiles_file = data_dir / "profiles.json" - self.profiles: Dict[str, Dict] = self._load_profiles() - - def _load_profiles(self) -> Dict[str, Dict]: - """Load profiles from file""" - if self.profiles_file.exists(): - try: - with open(self.profiles_file, "r", encoding="utf-8") as f: - profiles = json.load(f) - # Ensure default profile exists - if "default" not in profiles: - profiles["default"] = self._create_default_profile() - return profiles - except Exception as e: - print(f"Error loading profiles: {e}") - return {"default": self._create_default_profile()} - else: - # Create directory if it doesn't exist - self.profiles_file.parent.mkdir(parents=True, exist_ok=True) - # Create default profile - profiles = {"default": self._create_default_profile()} - self._save_profiles(profiles) - return profiles - - def _create_default_profile(self) -> Dict[str, Any]: - """Create default profile with timestamp""" - profile = self.DEFAULT_PROFILE.copy() - now = datetime.now().isoformat() - profile["created_at"] = now - profile["updated_at"] = now - return profile - - def _save_profiles(self, profiles: Optional[Dict] = None): - """Save profiles to file""" - if profiles is None: - profiles = self.profiles - try: - print(f"Saving profiles to: {self.profiles_file}") # Agregar debug - with open(self.profiles_file, "w", encoding="utf-8") as f: - json.dump(profiles, f, indent=4) - print("Profiles saved successfully") # Agregar debug - except Exception as e: - print(f"Error saving profiles: {e}") # Agregar debug - raise # Re-lanzar la excepción para que se maneje arriba - - def get_all_profiles(self) -> List[Dict[str, Any]]: - """Get all profiles""" - return list(self.profiles.values()) - - def get_profile(self, profile_id: str) -> Optional[Dict[str, Any]]: - """Get specific profile""" - return self.profiles.get(profile_id) - - def create_profile(self, profile_data: Dict[str, Any]) -> Dict[str, Any]: - """Create new profile""" - if "id" not in profile_data: - raise ValueError("Profile must have an id") - - profile_id = profile_data["id"] - if profile_id in self.profiles: - raise ValueError(f"Profile {profile_id} already exists") - - # Add timestamps - now = datetime.now().isoformat() - profile_data["created_at"] = now - profile_data["updated_at"] = now - - # Ensure required fields - for key in ["name", "llm_settings"]: - if key not in profile_data: - profile_data[key] = self.DEFAULT_PROFILE[key] - - self.profiles[profile_id] = profile_data - self._save_profiles() - return profile_data - - def update_profile( - self, profile_id: str, profile_data: Dict[str, Any] - ) -> Dict[str, Any]: - """Update existing profile""" - try: - print(f"Updating profile {profile_id} with data: {profile_data}") - if profile_id not in self.profiles: - raise ValueError(f"Profile {profile_id} not found") - - if profile_id == "default" and "id" in profile_data: - raise ValueError("Cannot change id of default profile") - - # Update timestamp - profile_data["updated_at"] = datetime.now().isoformat() - - # Update profile - current_profile = self.profiles[profile_id].copy() # Hacer una copia - current_profile.update(profile_data) # Actualizar la copia - self.profiles[profile_id] = current_profile # Asignar la copia actualizada - - print(f"Updated profile: {self.profiles[profile_id]}") - self._save_profiles() - return self.profiles[profile_id] - except Exception as e: - print(f"Error in update_profile: {e}") # Agregar debug - raise - - def delete_profile(self, profile_id: str): - """Delete profile""" - if profile_id == "default": - raise ValueError("Cannot delete default profile") - - if profile_id not in self.profiles: - raise ValueError(f"Profile {profile_id} not found") - - del self.profiles[profile_id] - self._save_profiles() diff --git a/claude/profiles.json b/claude/profiles.json deleted file mode 100644 index 3b68f75..0000000 --- a/claude/profiles.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "default": { - "id": "default", - "name": "Default Profile", - "work_dir": "", - "llm_settings": { - "model": "gpt-4", - "temperature": 0.7, - "api_key": "" - }, - "created_at": "2025-02-07T12:47:49.766608", - "updated_at": "2025-02-07T12:47:49.766608" - }, - "1": { - "id": "1", - "name": "Base", - "work_dir": "C:/Estudio", - "llm_settings": { - "api_key": "333333333333", - "model": "gpt-4", - "temperature": 0.7 - }, - "created_at": "2025-02-07T13:00:43.541932", - "updated_at": "2025-02-07T23:34:43.039269" - } -} \ No newline at end of file diff --git a/claude/project_structure.txt b/claude/project_structure.txt deleted file mode 100644 index 46cb7fe..0000000 --- a/claude/project_structure.txt +++ /dev/null @@ -1,39 +0,0 @@ -Estructura del proyecto original: - -└── LocalScriptsWeb - ├── backend - │ ├── core - │ │ ├── __init__.py - │ │ ├── directory_handler.py - │ │ ├── group_settings_manager.py - │ │ ├── profile_manager.py - │ │ ├── script_manager.py - │ │ └── workdir_config.py - │ ├── script_groups - │ │ ├── example_group - │ │ │ ├── __init__.py - │ │ │ ├── config.json - │ │ │ ├── x1.py - │ │ │ └── x2.py - │ │ ├── __init__.py - │ │ ├── base_script.py - │ │ └── config.json - │ ├── __init__.py - │ └── app.py - ├── data - │ └── profiles.json - ├── frontend - │ ├── static - │ │ ├── css - │ │ │ └── style.css - │ │ └── js - │ │ ├── main.js - │ │ ├── modal.js - │ │ ├── profile.js - │ │ ├── scripts.js - │ │ └── workdir_config.js - │ └── templates - │ ├── base.html - │ └── index.html - ├── claude_file_organizer.py - └── files.txt \ No newline at end of file diff --git a/claude/script_manager.py b/claude/script_manager.py deleted file mode 100644 index 03d83a6..0000000 --- a/claude/script_manager.py +++ /dev/null @@ -1,189 +0,0 @@ -# backend/core/script_manager.py -from pathlib import Path -import importlib.util -import inspect -from typing import Dict, List, Any, Optional -import json -from .group_settings_manager import GroupSettingsManager # Agregar esta importación - - -class ScriptManager: - def __init__(self, script_groups_dir: Path): - self.script_groups_dir = script_groups_dir - self.group_settings = GroupSettingsManager(script_groups_dir) - - def get_group_settings(self, group_id: str) -> Dict[str, Any]: - """Get settings for a script group""" - return self.group_settings.get_group_settings(group_id) - - def update_group_settings(self, group_id: str, settings: Dict[str, Any]): - """Update settings for a script group""" - return self.group_settings.update_group_settings(group_id, settings) - - def get_group_config_schema(self, group_id: str) -> Dict[str, Any]: - """Get configuration schema for a script group""" - config_file = self.script_groups_dir / group_id / "config.json" - print(f"Looking for config file: {config_file}") # Debug - - if config_file.exists(): - try: - with open(config_file, "r", encoding="utf-8") as f: - schema = json.load(f) - print(f"Loaded schema: {schema}") # Debug - return schema - except Exception as e: - print(f"Error loading group config schema: {e}") # Debug - else: - print(f"Config file not found: {config_file}") # Debug - - # Retornar un schema vacío si no existe el archivo - return {"group_name": group_id, "description": "", "config_schema": {}} - - def get_available_groups(self) -> List[Dict[str, Any]]: - """Get list of available script groups""" - groups = [] - - for group_dir in self.script_groups_dir.iterdir(): - if group_dir.is_dir() and not group_dir.name.startswith("_"): - groups.append( - { - "id": group_dir.name, - "name": group_dir.name.replace("_", " ").title(), - "path": str(group_dir), - } - ) - - return groups - - def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]: - """Get scripts for a specific group""" - group_dir = self.script_groups_dir / group_id - print(f"Looking for scripts in: {group_dir}") # Debug - - if not group_dir.exists() or not group_dir.is_dir(): - print(f"Directory not found: {group_dir}") # Debug - raise ValueError(f"Script group '{group_id}' not found") - - scripts = [] - for script_file in group_dir.glob("x[0-9].py"): - print(f"Found script file: {script_file}") # Debug - script_info = self._analyze_script(script_file) - if script_info: - scripts.append(script_info) - - return sorted(scripts, key=lambda x: x["id"]) - - def discover_groups(self) -> List[Dict[str, Any]]: - """Discover all script groups""" - groups = [] - - for group_dir in self.script_groups_dir.iterdir(): - if group_dir.is_dir() and not group_dir.name.startswith("_"): - group_info = self._analyze_group(group_dir) - if group_info: - groups.append(group_info) - - return groups - - def _analyze_group(self, group_dir: Path) -> Optional[Dict[str, Any]]: - """Analyze a script group directory""" - scripts = [] - - for script_file in group_dir.glob("x[0-9].py"): - try: - script_info = self._analyze_script(script_file) - if script_info: - scripts.append(script_info) - except Exception as e: - print(f"Error analyzing script {script_file}: {e}") - - if scripts: - return { - "id": group_dir.name, - "name": group_dir.name.replace("_", " ").title(), - "scripts": sorted(scripts, key=lambda x: x["id"]), - } - return None - - def _analyze_script(self, script_file: Path) -> Optional[Dict[str, Any]]: - """Analyze a single script file""" - try: - # Import script module - spec = importlib.util.spec_from_file_location(script_file.stem, script_file) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find script class - script_class = None - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and hasattr(obj, "run") - ): - script_class = obj - break - - if script_class: - # Extraer la primera línea del docstring como nombre - docstring = inspect.getdoc(script_class) - if docstring: - name, *description = docstring.split("\n", 1) - description = description[0] if description else "" - else: - name = script_file.stem - description = "" - - return { - "id": script_file.stem, - "name": name.strip(), - "description": description.strip(), - "file": str(script_file.relative_to(self.script_groups_dir)), - } - - except Exception as e: - print(f"Error loading script {script_file}: {e}") - - return None - - def execute_script( - self, group_id: str, script_id: str, profile: Dict[str, Any] - ) -> Dict[str, Any]: - """Execute a specific script""" - # Get group settings first - group_settings = self.group_settings.get_group_settings(group_id) - work_dir = group_settings.get("work_dir") - - if not work_dir: - raise ValueError(f"No work directory configured for group {group_id}") - - script_file = self.script_groups_dir / group_id / f"{script_id}.py" - - if not script_file.exists(): - raise ValueError(f"Script {script_id} not found in group {group_id}") - - try: - # Import script module - spec = importlib.util.spec_from_file_location(script_id, script_file) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # Find and instantiate script class - script_class = None - for name, obj in inspect.getmembers(module): - if ( - inspect.isclass(obj) - and obj.__module__ == module.__name__ - and hasattr(obj, "run") - ): - script_class = obj - break - - if not script_class: - raise ValueError(f"No valid script class found in {script_id}") - - script = script_class() - return script.run(work_dir, profile) - - except Exception as e: - return {"status": "error", "error": str(e)} diff --git a/claude/scripts.js b/claude/scripts.js deleted file mode 100644 index 4f85782..0000000 --- a/claude/scripts.js +++ /dev/null @@ -1,815 +0,0 @@ -// frontend/static/js/scripts.js - -// Script groups state -let scriptGroups = []; - -// Load script groups when page loads -document.addEventListener('DOMContentLoaded', async () => { - await loadScriptGroups(); -}); - -// Load script groups when page loads -document.addEventListener('DOMContentLoaded', async () => { - await loadScriptGroups(); -}); - -async function loadScriptGroups() { - try { - // Obtener los grupos desde el servidor - const groups = await apiRequest('/script-groups'); - console.log('Loaded script groups:', groups); - - // Obtener el selector y el último grupo seleccionado - const select = document.getElementById('groupSelect'); - const lastGroupId = localStorage.getItem('lastGroupId'); - console.log('Last group ID:', lastGroupId); - - // Remover event listener anterior si existe - select.removeEventListener('change', handleGroupChange); - - // Construir las opciones - select.innerHTML = ` - <option value="">Seleccionar grupo...</option> - ${groups.map(group => ` - <option value="${group.id}" ${group.id === lastGroupId ? 'selected' : ''}> - ${group.name} - </option> - `).join('')} - `; - - // Agregar event listener para cambios - select.addEventListener('change', handleGroupChange); - console.log('Added change event listener to groupSelect'); - - // Si hay un grupo guardado, cargarlo - if (lastGroupId) { - console.log('Loading last group scripts:', lastGroupId); - await loadGroupScripts(lastGroupId); - } - - } catch (error) { - console.error('Error al cargar grupos de scripts:', error); - showError('Error al cargar grupos de scripts'); - } -} - - -// Función para manejar el cambio de grupo -async function handleGroupChange(event) { - const groupId = event.target.value; - console.log('Group selection changed:', groupId); - - if (groupId) { - localStorage.setItem('lastGroupId', groupId); - console.log('Saved lastGroupId:', groupId); - } else { - localStorage.removeItem('lastGroupId'); - console.log('Removed lastGroupId'); - } - - await loadGroupScripts(groupId); -} - -// Actualizar función de cambio de perfil para mantener la persistencia -async function changeProfile() { - const select = document.getElementById('profileSelect'); - if (select.value) { - await selectProfile(select.value); - localStorage.setItem('lastProfileId', select.value); - - // Al cambiar de perfil, intentamos mantener el último grupo seleccionado - const lastGroupId = localStorage.getItem('lastGroupId'); - if (lastGroupId) { - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - await loadGroupScripts(lastGroupId); - } - } - } -} - -async function loadGroupScripts(groupId) { - const scriptList = document.getElementById('scriptList'); - - if (!groupId) { - scriptList.style.display = 'none'; - localStorage.removeItem('lastGroupId'); // Limpiar selección - return; - } - - // Guardar grupo seleccionado - localStorage.setItem('lastGroupId', groupId); - console.log('Group saved:', groupId); - - if (!currentProfile?.work_dir) { - scriptList.innerHTML = ` - <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4"> - <div class="flex"> - <div class="ml-3"> - <p class="text-sm text-yellow-700"> - Por favor, seleccione primero un directorio de trabajo - </p> - </div> - </div> - </div> - `; - scriptList.style.display = 'block'; - return; - } - - try { - console.log('Loading data for group:', groupId); - - // Actualizar el selector para reflejar la selección actual - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect && groupSelect.value !== groupId) { - groupSelect.value = groupId; - } - - // Cargar y loguear scripts - let groupScripts, configSchema; - try { - groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); - console.log('Scripts loaded:', groupScripts); - } catch (e) { - console.error('Error loading scripts:', e); - throw e; - } - - try { - configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); - console.log('Config schema loaded:', configSchema); - } catch (e) { - console.error('Error loading config schema:', e); - throw e; - } - - // Intentar cargar configuración actual - let currentConfig = {}; - try { - currentConfig = await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - console.log('Current config loaded:', currentConfig); - } catch (e) { - console.warn('No existing configuration found, using defaults'); - } - - // Verificar que tenemos los datos necesarios - if (!groupScripts || !configSchema) { - throw new Error('Failed to load required data'); - } - - console.log('Rendering UI with:', { - groupScripts, - configSchema, - currentConfig - }); - - scriptList.innerHTML = ` - <!-- Sección de Configuración --> - <div class="mb-6 bg-white shadow sm:rounded-lg"> - <div class="border-b border-gray-200 p-4 flex justify-between items-center"> - <div> - <h3 class="text-lg font-medium text-gray-900"> - ${configSchema.group_name || 'Configuración'} - </h3> - <p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p> - </div> - <button onclick="editConfigSchema('${groupId}')" - class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> - </svg> - Editar Esquema - </button> - </div> - <div class="p-4"> - <form id="groupConfigForm" class="grid grid-cols-2 gap-4"> - ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` - <div class="space-y-2 col-span-2"> - <label class="block text-sm font-medium text-gray-700"> - ${field.description} - </label> - ${generateFormField(key, field, currentConfig[key])} - </div> - `).join('')} - <div class="col-span-2 flex justify-end pt-4"> - <button type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Configuración - </button> - </div> - </form> - </div> - </div> - - <!-- Lista de Scripts --> - <div class="space-y-4"> - ${groupScripts.map(script => ` - <div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg"> - <div class="flex justify-between items-start"> - <div> - <h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4> - <p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p> - </div> - <button onclick="runScript('${groupId}', '${script.id}')" - class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Ejecutar - </button> - </div> - </div> - `).join('')} - </div>`; - - scriptList.style.display = 'block'; - - // Agregar evento para guardar configuración - const form = document.getElementById('groupConfigForm'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - await saveGroupConfig(groupId, form); - }); - - } catch (error) { - console.error('Error in loadGroupScripts:', error); - showError('Failed to load scripts and configuration'); - } -} - -// Update script groups display -function updateScriptGroupsDisplay() { - const container = document.getElementById('scriptGroups'); - - if (!scriptGroups.length) { - container.innerHTML = '<p class="no-scripts">No script groups available</p>'; - return; - } - - container.innerHTML = scriptGroups.map(group => ` - <div class="script-group" data-group-id="${group.id}"> - <div class="script-group-header"> - <h3>${group.name}</h3> - <button onclick="configureGroup('${group.id}')" class="config-btn"> - Configure - </button> - </div> - <div class="script-list"> - ${group.scripts.map(script => ` - <div class="script-item"> - <div class="script-info"> - <h4>${script.name}</h4> - <p>${script.description || 'No description available'}</p> - </div> - <div class="script-actions"> - <button onclick="runScript('${group.id}', '${script.id}')" class="run-btn"> - Run - </button> - </div> - </div> - `).join('')} - </div> - </div> - `).join(''); -} - -// Run a script -async function runScript(groupId, scriptId) { - if (!currentProfile?.work_dir) { - showError('Please select a work directory first'); - return; - } - - try { - const result = await apiRequest(`/scripts/${groupId}/${scriptId}/run`, { - method: 'POST', - body: JSON.stringify({ - work_dir: currentProfile.work_dir, - profile: currentProfile - }) - }); - - if (result.status === 'error') { - showError(result.error); - } else { - showSuccess(`Script ${scriptId} executed successfully`); - if (result.output) { - const output = document.getElementById('outputArea'); - output.innerHTML += `\n[${new Date().toLocaleTimeString()}] ${result.output}`; - output.scrollTop = output.scrollHeight; - } - } - } catch (error) { - showError(`Failed to run script: ${error.message}`); - } -} - -// Configure script group -async function configureGroup(groupId) { - if (!currentProfile?.work_dir) { - showError('Please select a work directory first'); - return; - } - - try { - const config = await getGroupConfig(groupId); - showGroupConfigEditor(groupId, config); - } catch (error) { - showError('Failed to load group configuration'); - } -} - -// Show group configuration editor -function showGroupConfigEditor(groupId, config) { - const group = scriptGroups.find(g => g.id === groupId); - if (!group) return; - - const modal = document.createElement('div'); - modal.className = 'modal active'; - - modal.innerHTML = ` - <div class="modal-content"> - <h2>${group.name} Configuration</h2> - <form id="groupConfigForm" onsubmit="saveGroupConfig(event, '${groupId}')"> - <div class="form-group"> - <label for="configData">Configuration (JSON)</label> - <textarea id="configData" name="config" rows="10" - class="config-editor">${JSON.stringify(config || {}, null, 2)}</textarea> - </div> - <div class="button-group"> - <button type="button" onclick="closeModal(this)">Cancel</button> - <button type="submit">Save</button> - </div> - </form> - </div> - `; - - document.body.appendChild(modal); -} - -function generateFormField(key, field, currentValue) { - switch (field.type) { - case 'string': - return ` - <input type="text" - name="${key}" - value="${currentValue || ''}" - class="${STYLES.editableInput}"> - `; - case 'number': - return ` - <input type="number" - name="${key}" - value="${currentValue || 0}" - class="${STYLES.editableInput}"> - `; - case 'boolean': - return ` - <select name="${key}" class="${STYLES.editableInput}"> - <option value="true" ${currentValue ? 'selected' : ''}>Yes</option> - <option value="false" ${!currentValue ? 'selected' : ''}>No</option> - </select> - `; - case 'select': - return ` - <select name="${key}" class="${STYLES.editableInput}"> - ${field.options.map(opt => ` - <option value="${opt}" ${currentValue === opt ? 'selected' : ''}> - ${opt} - </option> - `).join('')} - </select> - `; - default: - return `<input type="text" name="${key}" value="${currentValue || ''}" class="${STYLES.editableInput}">`; - } -} - -async function loadGroupScripts(groupId) { - const scriptList = document.getElementById('scriptList'); - - if (!groupId) { - scriptList.style.display = 'none'; - localStorage.removeItem('lastGroupId'); - return; - } - - if (!currentProfile?.work_dir) { - scriptList.innerHTML = ` - <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4"> - <div class="flex"> - <div class="ml-3"> - <p class="text-sm text-yellow-700"> - Por favor, seleccione primero un directorio de trabajo - </p> - </div> - </div> - </div> - `; - scriptList.style.display = 'block'; - return; - } - - try { - console.log('Loading data for group:', groupId); - - // Cargar y loguear scripts - let groupScripts, configSchema; - try { - groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); - console.log('Scripts loaded:', groupScripts); - } catch (e) { - console.error('Error loading scripts:', e); - throw e; - } - - try { - configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); - console.log('Config schema loaded:', configSchema); - } catch (e) { - console.error('Error loading config schema:', e); - throw e; - } - - // Intentar cargar configuración actual - let currentConfig = {}; - try { - console.log('Loading current config for work_dir:', currentProfile.work_dir); - currentConfig = await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - console.log('Current config loaded:', currentConfig); - } catch (e) { - console.warn('No existing configuration found, using defaults'); - } - - // Verificar que tenemos los datos necesarios - if (!groupScripts || !configSchema) { - throw new Error('Failed to load required data'); - } - - console.log('Rendering UI with:', { - groupScripts, - configSchema, - currentConfig - }); - - scriptList.innerHTML = ` - <!-- Sección de Configuración --> - <div class="mb-6 bg-white shadow sm:rounded-lg"> - <div class="border-b border-gray-200 p-4 flex justify-between items-center"> - <div> - <h3 class="text-lg font-medium text-gray-900"> - ${configSchema.group_name || 'Configuración'} - </h3> - <p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p> - </div> - <button onclick="editConfigSchema('${groupId}')" - class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> - </svg> - Editar Esquema - </button> - </div> - <div class="p-4"> - <form id="groupConfigForm" class="grid grid-cols-2 gap-4"> - ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` - <div class="space-y-2 col-span-2"> - <label class="block text-sm font-medium text-gray-700"> - ${field.description} - </label> - ${generateFormField(key, field, currentConfig[key])} - </div> - `).join('')} - <div class="col-span-2 flex justify-end pt-4"> - <button type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Configuración - </button> - </div> - </form> - </div> - </div> - - <!-- Lista de Scripts --> - <div class="space-y-4"> - ${groupScripts.map(script => ` - <div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg"> - <div class="flex justify-between items-start"> - <div> - <h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4> - <p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p> - </div> - <button onclick="runScript('${groupId}', '${script.id}')" - class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Ejecutar - </button> - </div> - </div> - `).join('')} - </div>`; - - scriptList.style.display = 'block'; - - // Agregar evento para guardar configuración - const form = document.getElementById('groupConfigForm'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - await saveGroupConfig(groupId, form); - }); - - } catch (error) { - console.error('Error in loadGroupScripts:', error); - showError('Failed to load scripts and configuration'); - } -} - -async function editConfigSchema(groupId) { - try { - const schema = await apiRequest(`/script-groups/${groupId}/config-schema`); - const configSection = document.createElement('div'); - configSection.id = 'schemaEditor'; - configSection.className = 'mb-6 bg-white shadow sm:rounded-lg'; - - configSection.innerHTML = ` - <div class="border-b border-gray-200 p-4 flex justify-between items-center bg-gray-50"> - <h3 class="text-lg font-medium text-gray-900">Editar Configuración del Esquema</h3> - <button onclick="this.closest('#schemaEditor').remove()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cerrar Editor - </button> - </div> - <div class="p-4"> - <div class="space-y-4"> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Nombre del Grupo</label> - <input type="text" name="group_name" value="${schema.group_name}" - class="${STYLES.editableInput}"> - </div> - <div class="text-right"> - <button onclick="addParameter(this)" - class="mt-6 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500"> - Agregar Parámetro - </button> - </div> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Descripción</label> - <input type="text" name="description" value="${schema.description}" - class="${STYLES.editableInput}"> - </div> - <div id="parameters" class="space-y-2"> - <div class="flex justify-between items-center"> - <h4 class="font-medium text-gray-900">Parámetros</h4> - </div> - ${Object.entries(schema.config_schema).map(([key, param]) => ` - <div class="parameter-item bg-gray-50 p-4 rounded-md relative"> - <button onclick="removeParameter(this)" - class="absolute top-2 right-2 text-red-600 hover:text-red-700"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> - </svg> - </button> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Parameter Name</label> - <input type="text" name="param_name" value="${key}" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Type</label> - <select name="param_type" - onchange="handleTypeChange(this)" - class="${STYLES.editableInput}"> - <option value="string" ${param.type === 'string' ? 'selected' : ''}>String</option> - <option value="number" ${param.type === 'number' ? 'selected' : ''}>Number</option> - <option value="boolean" ${param.type === 'boolean' ? 'selected' : ''}>Boolean</option> - <option value="select" ${param.type === 'select' ? 'selected' : ''}>Select</option> - </select> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Description</label> - <input type="text" name="param_description" value="${param.description}" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Default Value</label> - <input type="text" name="param_default" value="${param.default}" - class="${STYLES.editableInput}"> - </div> - ${param.type === 'select' ? ` - <div> - <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> - <input type="text" name="param_options" value="${param.options.join(', ')}" - class="${STYLES.editableInput}"> - </div> - ` : ''} - </div> - </div> - `).join('')} - </div> - <div class="flex justify-end space-x-3 pt-4 border-t border-gray-200"> - <button onclick="this.closest('#schemaEditor').remove()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cancelar - </button> - <button onclick="saveConfigSchema('${groupId}', this.closest('#schemaEditor'))" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Cambios - </button> - </div> - </div> - </div> - `; - - // Insertamos el editor justo después del botón "Edit Schema" - const scriptList = document.getElementById('scriptList'); - const existingEditor = document.getElementById('schemaEditor'); - if (existingEditor) { - existingEditor.remove(); - } - scriptList.insertBefore(configSection, scriptList.firstChild); - - } catch (error) { - showError('Error al cargar el esquema de configuración'); - } -} - -function createModal(title, content, onSave = null) { - const modal = document.createElement('div'); - modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4'; - - modal.innerHTML = ` - <div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col"> - <div class="px-6 py-4 border-b border-gray-200"> - <h3 class="text-lg font-medium text-gray-900">${title}</h3> - </div> - <div class="px-6 py-4 overflow-y-auto flex-1"> - ${content} - </div> - <div class="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end gap-3 border-t border-gray-200"> - <button onclick="closeModal(this)" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cancelar - </button> - ${onSave ? ` - <button onclick="saveModal(this)" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar - </button> - ` : ''} - </div> - </div> - `; - - document.body.appendChild(modal); - return modal; -} - -function addParameter(button) { - const parametersDiv = button.closest('.space-y-4').querySelector('#parameters'); - const newParam = document.createElement('div'); - newParam.className = 'parameter-item bg-gray-50 p-4 rounded-md relative'; - newParam.innerHTML = ` - <button onclick="removeParameter(this)" - class="absolute top-2 right-2 text-red-600 hover:text-red-700"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> - </svg> - </button> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Parameter Name</label> - <input type="text" name="param_name" value="" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Type</label> - <select name="param_type" - onchange="handleTypeChange(this)" - class="${STYLES.editableInput}"> - <option value="string">String</option> - <option value="number">Number</option> - <option value="boolean">Boolean</option> - <option value="select">Select</option> - </select> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Description</label> - <input type="text" name="param_description" value="" - class="${STYLES.editableInput}"> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Default Value</label> - <input type="text" name="param_default" value="" - class="${STYLES.editableInput}"> - </div> - </div> - `; - parametersDiv.appendChild(newParam); -} - -function removeParameter(button) { - button.closest('.parameter-item').remove(); -} - -function handleTypeChange(select) { - const paramItem = select.closest('.parameter-item'); - const optionsDiv = paramItem.querySelector('[name="param_options"]')?.closest('div'); - - if (select.value === 'select') { - if (!optionsDiv) { - const div = document.createElement('div'); - div.className = 'col-span-2'; - div.innerHTML = ` - <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> - <input type="text" name="param_options" value="" - class="${STYLES.editableInput}"> - `; - paramItem.querySelector('.grid').appendChild(div); - } - } else { - optionsDiv?.remove(); - } -} - -async function saveConfigSchema(groupId, modal) { - const form = modal.querySelector('div'); - const schema = { - group_name: form.querySelector('[name="group_name"]').value, - description: form.querySelector('[name="description"]').value, - config_schema: {} - }; - - // Recopilar parámetros - form.querySelectorAll('.parameter-item').forEach(item => { - const name = item.querySelector('[name="param_name"]').value; - const type = item.querySelector('[name="param_type"]').value; - const description = item.querySelector('[name="param_description"]').value; - const defaultValue = item.querySelector('[name="param_default"]').value; - - const param = { - type, - description, - default: type === 'boolean' ? defaultValue === 'true' : defaultValue - }; - - if (type === 'select') { - const options = item.querySelector('[name="param_options"]').value - .split(',') - .map(opt => opt.trim()) - .filter(Boolean); - param.options = options; - } - - schema.config_schema[name] = param; - }); - - try { - await apiRequest(`/script-groups/${groupId}/config-schema`, { - method: 'PUT', - body: JSON.stringify(schema) - }); - - closeModal(modal.querySelector('button')); - showSuccess('Configuration schema updated successfully'); - // Recargar la página para mostrar los cambios - loadGroupScripts(groupId); - } catch (error) { - showError('Failed to update configuration schema'); - } -} - -function showScriptForm(script) { - const modal = document.createElement('div'); - modal.className = 'modal active'; - - modal.innerHTML = ` - <div class="modal-content"> - <h2 class="text-xl font-bold mb-4">${script.name}</h2> - <form id="scriptForm" class="space-y-4"> - <div class="form-group"> - <label class="block text-sm font-medium text-gray-700">Parameters</label> - <textarea name="parameters" rows="4" - class="${STYLES.editableInput}" - placeholder="Enter script parameters (optional)"></textarea> - </div> - <div class="mt-4 flex justify-end space-x-3"> - <button type="button" onclick="closeModal(this)" - class="${STYLES.buttonSecondary}">Cancel</button> - <button type="submit" - class="${STYLES.button}">Run</button> - </div> - </form> - <div id="scriptOutput" class="mt-4 hidden"> - <h3 class="font-bold mb-2">Output:</h3> - <pre class="output-area p-4 bg-gray-100 rounded"></pre> - </div> - </div> - `; - - document.body.appendChild(modal); -} \ No newline at end of file diff --git a/claude/style.css b/claude/style.css deleted file mode 100644 index a343b9b..0000000 --- a/claude/style.css +++ /dev/null @@ -1,31 +0,0 @@ -/* frontend/static/css/style.css */ - -/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */ -.output-area { - white-space: pre-wrap; - word-wrap: break-word; -} - -/* Estilos para modales que no se pueden lograr fácilmente con Tailwind */ -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.modal-content { - background: white; - padding: 2rem; - border-radius: 0.5rem; - max-width: 600px; - width: 90%; - max-height: 90vh; - overflow-y: auto; -} \ No newline at end of file diff --git a/claude/workdir_config.js b/claude/workdir_config.js deleted file mode 100644 index 8d17144..0000000 --- a/claude/workdir_config.js +++ /dev/null @@ -1,161 +0,0 @@ -// frontend/static/js/workdir_config.js - -async function getWorkDirConfig() { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return null; - } - - try { - return await apiRequest(`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}`); - } catch (error) { - showError('Error al cargar la configuración del directorio de trabajo'); - return null; - } -} - -async function getGroupConfig(groupId) { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return null; - } - - try { - return await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - } catch (error) { - showError('Error al cargar la configuración del grupo'); - return null; - } -} - -async function updateGroupConfig(groupId, settings) { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return false; - } - - try { - await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`, - { - method: 'PUT', - body: JSON.stringify(settings) - } - ); - showSuccess('Group configuration updated successfully'); - return true; - } catch (error) { - showError('Failed to update group configuration'); - return false; - } -} - -function showConfigEditor(config, schema) { - const modal = document.createElement('div'); - modal.className = 'modal active'; - - const formContent = Object.entries(schema).map(([key, field]) => ` - <div class="form-group"> - <label class="block text-sm font-medium text-gray-700">${field.description || key}</label> - ${getInputByType(key, field, config[key])} - </div> - `).join(''); - - modal.innerHTML = ` - <div class="modal-content"> - <h2 class="text-xl font-bold mb-4">Work Directory Configuration</h2> - <form id="configForm" class="space-y-4"> - ${formContent} - <div class="mt-4 flex justify-end space-x-3"> - <button type="button" onclick="closeModal(this)" - class="${STYLES.buttonSecondary}">Cancel</button> - <button type="submit" - class="${STYLES.button}">Save</button> - </div> - </form> - </div> - `; - - document.body.appendChild(modal); -} - -function getInputByType(key, field, value) { - switch (field.type) { - case 'select': - return ` - <select name="${key}" - class="${STYLES.editableInput}"> - ${field.options.map(opt => ` - <option value="${opt}" ${value === opt ? 'selected' : ''}> - ${opt} - </option> - `).join('')} - </select>`; - case 'boolean': - return ` - <select name="${key}" - class="${STYLES.editableInput}"> - <option value="true" ${value ? 'selected' : ''}>Yes</option> - <option value="false" ${!value ? 'selected' : ''}>No</option> - </select>`; - case 'number': - return ` - <input type="number" name="${key}" - value="${value || field.default || ''}" - class="${STYLES.editableInput}">`; - default: - return ` - <input type="text" name="${key}" - value="${value || field.default || ''}" - class="${STYLES.editableInput}">`; - } -} - -// static/js/workdir_config.js -async function showWorkDirConfig() { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return; - } - - try { - const config = await getWorkDirConfig(); - - const content = ` - <div class="space-y-4"> - <div> - <h4 class="text-sm font-medium text-gray-900">Directory</h4> - <p class="mt-1 text-sm text-gray-500">${currentProfile.work_dir}</p> - </div> - <div> - <h4 class="text-sm font-medium text-gray-900">Version</h4> - <p class="mt-1 text-sm text-gray-500">${config.version}</p> - </div> - <div> - <h4 class="text-sm font-medium text-gray-900">Group Configurations</h4> - <div class="mt-2 space-y-3"> - ${Object.entries(config.group_settings || {}).map(([groupId, settings]) => ` - <div class="rounded-md bg-gray-50 p-3"> - <h5 class="text-sm font-medium text-gray-900">${groupId}</h5> - <pre class="mt-2 text-xs text-gray-500">${JSON.stringify(settings, null, 2)}</pre> - </div> - `).join('')} - </div> - </div> - </div> - `; - - createModal('Work Directory Configuration', content); - } catch (error) { - showError('Error al cargar la configuración del directorio de trabajo'); - } -} - -function closeModal(button) { - const modal = button.closest('.modal'); - if (modal) { - modal.remove(); - } -} \ No newline at end of file diff --git a/claude/workdir_config.py b/claude/workdir_config.py deleted file mode 100644 index cb34d6a..0000000 --- a/claude/workdir_config.py +++ /dev/null @@ -1,72 +0,0 @@ -# backend/core/workdir_config.py -from pathlib import Path -import json -from typing import Dict, Any, Optional -from datetime import datetime - -class WorkDirConfigManager: - """Manages configuration files in work directories""" - - DEFAULT_CONFIG = { - "version": "1.0", - "created_at": "", - "updated_at": "", - "group_settings": {} - } - - def __init__(self, work_dir: str): - self.work_dir = Path(work_dir) - self.config_file = self.work_dir / "script_config.json" - - def get_config(self) -> Dict[str, Any]: - """Get configuration for work directory""" - if self.config_file.exists(): - try: - with open(self.config_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"Error loading work dir config: {e}") - return self._create_default_config() - return self._create_default_config() - - def _create_default_config(self) -> Dict[str, Any]: - """Create default configuration""" - config = self.DEFAULT_CONFIG.copy() - now = datetime.now().isoformat() - config["created_at"] = now - config["updated_at"] = now - return config - - def save_config(self, config: Dict[str, Any]): - """Save configuration to file""" - # Ensure work directory exists - self.work_dir.mkdir(parents=True, exist_ok=True) - - # Update timestamp - config["updated_at"] = datetime.now().isoformat() - - # Save config - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=4) - - def get_group_config(self, group_id: str) -> Dict[str, Any]: - """Get configuration for specific script group""" - config = self.get_config() - return config["group_settings"].get(group_id, {}) - - def update_group_config(self, group_id: str, settings: Dict[str, Any]): - """Update configuration for specific script group""" - config = self.get_config() - - if "group_settings" not in config: - config["group_settings"] = {} - - config["group_settings"][group_id] = settings - self.save_config(config) - - def remove_group_config(self, group_id: str): - """Remove configuration for specific script group""" - config = self.get_config() - if group_id in config.get("group_settings", {}): - del config["group_settings"][group_id] - self.save_config(config) \ No newline at end of file diff --git a/claude/x1.py b/claude/x1.py deleted file mode 100644 index 257c8de..0000000 --- a/claude/x1.py +++ /dev/null @@ -1,108 +0,0 @@ -# backend/script_groups/example_group/x1.py -from backend.script_groups.base_script import BaseScript -import os -from pathlib import Path -import json -import csv -from datetime import datetime - -class FileCounter(BaseScript): - """ - File Analysis - Analyzes files in directory with configurable filters and reporting - """ - - def run(self, work_dir: str, profile: dict) -> dict: - try: - # Get configuration - config = self.get_config(work_dir, "example_group") - - # Process configuration values - exclude_dirs = [d.strip() for d in config.get("exclude_dirs", "").split(",") if d.strip()] - count_hidden = config.get("count_hidden", False) - min_size = config.get("min_size", 0) - save_report = config.get("save_report", True) - report_format = config.get("report_format", "json") - - # Initialize counters - extension_counts = {} - total_files = 0 - total_size = 0 - skipped_files = 0 - - # Walk through directory - for root, dirs, files in os.walk(work_dir): - # Skip excluded directories - dirs[:] = [d for d in dirs if d not in exclude_dirs] - - for file in files: - file_path = Path(root) / file - - # Skip hidden files if not counting them - if not count_hidden and file.startswith('.'): - skipped_files += 1 - continue - - # Check file size - try: - file_size = file_path.stat().st_size - if file_size < min_size: - skipped_files += 1 - continue - except: - continue - - # Count file - total_files += 1 - total_size += file_size - ext = file_path.suffix.lower() or 'no extension' - extension_counts[ext] = extension_counts.get(ext, 0) + 1 - - # Prepare results - results = { - "scan_time": datetime.now().isoformat(), - "total_files": total_files, - "total_size": total_size, - "skipped_files": skipped_files, - "extension_counts": extension_counts - } - - # Save report if configured - if save_report: - report_path = Path(work_dir) / f"file_analysis.{report_format}" - if report_format == "json": - with open(report_path, 'w') as f: - json.dump(results, f, indent=2) - elif report_format == "csv": - with open(report_path, 'w', newline='') as f: - writer = csv.writer(f) - writer.writerow(["Extension", "Count"]) - for ext, count in sorted(extension_counts.items()): - writer.writerow([ext, count]) - else: # txt - with open(report_path, 'w') as f: - f.write(f"File Analysis Report\n") - f.write(f"Generated: {results['scan_time']}\n\n") - f.write(f"Total Files: {total_files}\n") - f.write(f"Total Size: {total_size:,} bytes\n") - f.write(f"Skipped Files: {skipped_files}\n\n") - f.write("Extension Counts:\n") - for ext, count in sorted(extension_counts.items()): - f.write(f"{ext}: {count}\n") - - return { - "status": "success", - "data": results, - "output": f"Found {total_files:,} files ({total_size:,} bytes)\n" + - f"Skipped {skipped_files} files\n\n" + - "Extensions:\n" + "\n".join( - f"{ext}: {count:,} files" - for ext, count in sorted(extension_counts.items()) - ) - } - - except Exception as e: - return { - "status": "error", - "error": str(e) - } \ No newline at end of file diff --git a/claude/x2.py b/claude/x2.py deleted file mode 100644 index c3b805b..0000000 --- a/claude/x2.py +++ /dev/null @@ -1,104 +0,0 @@ -# backend/script_groups/example_group/x2.py -from backend.script_groups.base_script import BaseScript -import psutil -import json -from datetime import datetime -from pathlib import Path - -class SystemInfo(BaseScript): - """ - System Monitor - Collects and analyzes system performance metrics - """ - - def run(self, work_dir: str, profile: dict) -> dict: - try: - # Get configuration from the same config.json - config = self.get_config(work_dir, "example_group") - save_report = config.get("save_report", True) - report_format = config.get("report_format", "json") - - # Collect system information - cpu_freq = psutil.cpu_freq() - memory = psutil.virtual_memory() - disk = psutil.disk_usage(work_dir) - - info = { - "timestamp": datetime.now().isoformat(), - "cpu": { - "cores": psutil.cpu_count(), - "physical_cores": psutil.cpu_count(logical=False), - "frequency": { - "current": round(cpu_freq.current, 2) if cpu_freq else None, - "min": round(cpu_freq.min, 2) if cpu_freq else None, - "max": round(cpu_freq.max, 2) if cpu_freq else None - }, - "usage_percent": psutil.cpu_percent(interval=1) - }, - "memory": { - "total": memory.total, - "available": memory.available, - "used": memory.used, - "percent": memory.percent - }, - "disk": { - "total": disk.total, - "used": disk.used, - "free": disk.free, - "percent": disk.percent - }, - "network": { - "interfaces": list(psutil.net_if_addrs().keys()), - "connections": len(psutil.net_connections()) - } - } - - # Save report if configured - if save_report: - report_path = Path(work_dir) / f"system_info.{report_format}" - if report_format == "json": - with open(report_path, 'w') as f: - json.dump(info, f, indent=2) - elif report_format == "csv": - with open(report_path, 'w', newline='') as f: - writer = csv.writer(f) - writer.writerow(["Metric", "Value"]) - writer.writerow(["CPU Cores", info["cpu"]["cores"]]) - writer.writerow(["CPU Usage", f"{info['cpu']['usage_percent']}%"]) - writer.writerow(["Memory Total", f"{info['memory']['total']:,} bytes"]) - writer.writerow(["Memory Used", f"{info['memory']['percent']}%"]) - writer.writerow(["Disk Total", f"{info['disk']['total']:,} bytes"]) - writer.writerow(["Disk Used", f"{info['disk']['percent']}%"]) - else: # txt - with open(report_path, 'w') as f: - f.write(f"System Information Report\n") - f.write(f"Generated: {info['timestamp']}\n\n") - f.write(f"CPU:\n") - f.write(f" Cores: {info['cpu']['cores']}\n") - f.write(f" Usage: {info['cpu']['usage_percent']}%\n\n") - f.write(f"Memory:\n") - f.write(f" Total: {info['memory']['total']:,} bytes\n") - f.write(f" Used: {info['memory']['percent']}%\n\n") - f.write(f"Disk:\n") - f.write(f" Total: {info['disk']['total']:,} bytes\n") - f.write(f" Used: {info['disk']['percent']}%\n") - - # Format output - output = f"""System Information: -CPU: {info['cpu']['cores']} cores ({info['cpu']['usage_percent']}% usage) -Memory: {info['memory']['percent']}% used ({info['memory']['available']:,} bytes available) -Disk: {info['disk']['percent']}% used ({info['disk']['free']:,} bytes free) -Network Interfaces: {', '.join(info['network']['interfaces'])} -Active Connections: {info['network']['connections']}""" - - return { - "status": "success", - "data": info, - "output": output - } - - except Exception as e: - return { - "status": "error", - "error": str(e) - } \ No newline at end of file diff --git a/data/profile_schema.json b/data/profile_schema.json new file mode 100644 index 0000000..522d87f --- /dev/null +++ b/data/profile_schema.json @@ -0,0 +1,39 @@ +{ + "description": "Configuration schema for application profiles", + "config_schema": { + "id": { + "type": "string", + "description": "Unique identifier for the profile", + "required": true + }, + "name": { + "type": "string", + "description": "Display name for the profile", + "required": true + }, + "llm_settings": { + "type": "object", + "description": "Language model settings", + "properties": { + "model": { + "type": "select", + "description": "Language model to use", + "options": ["gpt-4", "gpt-3.5-turbo"], + "default": "gpt-4" + }, + "temperature": { + "type": "number", + "description": "Temperature for text generation", + "default": 0.7, + "min": 0, + "max": 2 + }, + "api_key": { + "type": "string", + "description": "API key for the language model", + "default": "" + } + } + } + } +} \ No newline at end of file diff --git a/data/profiles.json b/data/profiles.json index 3b68f75..0e256b3 100644 --- a/data/profiles.json +++ b/data/profiles.json @@ -2,25 +2,23 @@ "default": { "id": "default", "name": "Default Profile", - "work_dir": "", "llm_settings": { "model": "gpt-4", "temperature": 0.7, "api_key": "" }, - "created_at": "2025-02-07T12:47:49.766608", - "updated_at": "2025-02-07T12:47:49.766608" + "created_at": "2025-02-08T12:00:00.000Z", + "updated_at": "2025-02-08T12:00:00.000Z" }, "1": { "id": "1", "name": "Base", - "work_dir": "C:/Estudio", "llm_settings": { "api_key": "333333333333", "model": "gpt-4", "temperature": 0.7 }, - "created_at": "2025-02-07T13:00:43.541932", - "updated_at": "2025-02-07T23:34:43.039269" + "created_at": "2025-02-08T13:00:43.541932", + "updated_at": "2025-02-08T23:34:43.039269" } } \ No newline at end of file diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css index a343b9b..78339b4 100644 --- a/frontend/static/css/style.css +++ b/frontend/static/css/style.css @@ -1,12 +1,12 @@ /* frontend/static/css/style.css */ -/* Solo mantenemos estilos específicos que no podemos lograr fácilmente con Tailwind */ +/* Estilos para el área de salida que requiere white-space específico */ .output-area { white-space: pre-wrap; word-wrap: break-word; } -/* Estilos para modales que no se pueden lograr fácilmente con Tailwind */ +/* Estilos para modales - solo lo que no se puede hacer con Tailwind */ .modal { position: fixed; top: 0; @@ -21,11 +21,6 @@ } .modal-content { - background: white; - padding: 2rem; - border-radius: 0.5rem; - max-width: 600px; - width: 90%; max-height: 90vh; overflow-y: auto; } \ No newline at end of file diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js index 951ab99..3737acf 100644 --- a/frontend/static/js/main.js +++ b/frontend/static/js/main.js @@ -1,9 +1,10 @@ // frontend/static/js/main.js -// Global state +// Estado global let currentProfile = null; +let currentGroup = null; -// Definir clases comunes para inputs +// Estilos comunes const STYLES = { editableInput: "mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500", readonlyInput: "mt-1 block w-full rounded-md border-2 border-gray-200 bg-gray-100 px-3 py-2 shadow-sm", @@ -11,9 +12,10 @@ const STYLES = { buttonSecondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300" }; +// Inicialización de la aplicación async function initializeApp() { try { - console.log('Inicializando aplicación...'); + console.log('Initializing application...'); // Cargar perfiles const profiles = await apiRequest('/profiles'); @@ -33,66 +35,16 @@ async function initializeApp() { await selectProfile(selectedProfile.id); } - // Cargar grupos de scripts y restaurar la última selección - await restoreScriptGroup(); - - // Actualizar la interfaz - updateWorkDirDisplay(); + // Restaurar último estado + await restoreLastState(); } catch (error) { - console.error('Error al inicializar la aplicación:', error); - showError('Error al inicializar la aplicación'); + console.error('Error initializing application:', error); + showError('Error initializing application'); } } -async function restoreScriptGroup() { - try { - // Primero cargar los grupos disponibles - await loadScriptGroups(); - - // Luego intentar restaurar el último grupo seleccionado - const lastGroupId = localStorage.getItem('lastGroupId'); - if (lastGroupId) { - console.log('Restoring last group:', lastGroupId); - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - if (groupSelect.value) { // Verifica que el valor se haya establecido correctamente - await loadGroupScripts(lastGroupId); - } else { - console.log('Selected group no longer exists:', lastGroupId); - localStorage.removeItem('lastGroupId'); - } - } - } - } catch (error) { - console.error('Error restoring script group:', error); - } -} - -// Función para restaurar el último estado -async function restoreLastState() { - const lastProfileId = localStorage.getItem('lastProfileId'); - const lastGroupId = localStorage.getItem('lastGroupId'); - - console.log('Restoring last state:', { lastProfileId, lastGroupId }); - - if (lastProfileId) { - const profileSelect = document.getElementById('profileSelect'); - profileSelect.value = lastProfileId; - await selectProfile(lastProfileId); - } - - if (lastGroupId) { - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - await loadGroupScripts(lastGroupId); - } - } -} - -// API functions +// Funciones de API async function apiRequest(endpoint, options = {}) { try { const response = await fetch(`/api${endpoint}`, { @@ -105,127 +57,110 @@ async function apiRequest(endpoint, options = {}) { if (!response.ok) { const error = await response.json(); - throw new Error(error.error || 'Error en la solicitud API'); + throw new Error(error.error || 'API request error'); } return await response.json(); } catch (error) { - console.error('Error API:', error); + console.error('API Error:', error); showError(error.message); throw error; } } -async function loadProfiles() { - try { - const profiles = await apiRequest('/profiles'); - updateProfileSelector(profiles); - - // Obtener último perfil usado - const lastProfileId = localStorage.getItem('lastProfileId'); - - // Seleccionar perfil guardado o el default - const defaultProfile = profiles.find(p => p.id === (lastProfileId || 'default')) || profiles[0]; - if (defaultProfile) { - await selectProfile(defaultProfile.id); - } - } catch (error) { - showError('Error al cargar los perfiles'); - } -} - +// Funciones de gestión de perfiles async function selectProfile(profileId) { try { - console.log('Seleccionando perfil:', profileId); + console.log('Selecting profile:', profileId); + + // Cargar perfil currentProfile = await apiRequest(`/profiles/${profileId}`); // Guardar en localStorage localStorage.setItem('lastProfileId', profileId); - console.log('Profile ID saved to storage:', profileId); + console.log('Profile ID saved:', profileId); - // Actualizar explícitamente el valor del combo - const select = document.getElementById('profileSelect'); - if (select) { - select.value = profileId; - console.log('Updated profileSelect value to:', profileId); - } + // Actualizar UI + updateProfileSelector([currentProfile]); + updateProfileDisplay(); - updateWorkDirDisplay(); - - // Recargar scripts con el último grupo seleccionado - await restoreScriptGroup(); + return currentProfile; } catch (error) { - console.error('Error al seleccionar perfil:', error); - showError('Error al cargar el perfil'); + console.error('Error selecting profile:', error); + showError('Error loading profile'); + throw error; } } -// Initialize when page loads -document.addEventListener('DOMContentLoaded', initializeApp); - function updateProfileSelector(profiles) { const select = document.getElementById('profileSelect'); + if (!select) return; + const lastProfileId = localStorage.getItem('lastProfileId') || 'default'; - console.log('Updating profile selector. Last profile ID:', lastProfileId); - - // Construir las opciones select.innerHTML = profiles.map(profile => ` <option value="${profile.id}" ${profile.id === lastProfileId ? 'selected' : ''}> ${profile.name} </option> `).join(''); - - // Asegurar que el valor seleccionado sea correcto - select.value = lastProfileId; - console.log('Set profileSelect value to:', lastProfileId); } -async function changeProfile() { - const select = document.getElementById('profileSelect'); - if (select.value) { - await selectProfile(select.value); - await loadScriptGroups(); // Reload scripts when profile changes - } +function updateProfileDisplay() { + const container = document.getElementById('profileConfig'); + if (!container || !currentProfile) return; + + container.innerHTML = ` + <div class="space-y-4"> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-700">Profile ID</label> + <input type="text" value="${currentProfile.id}" readonly + class="${STYLES.readonlyInput}"> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Name</label> + <input type="text" value="${currentProfile.name}" readonly + class="${STYLES.readonlyInput}"> + </div> + </div> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-700">Model</label> + <input type="text" value="${currentProfile.llm_settings?.model || ''}" readonly + class="${STYLES.readonlyInput}"> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Temperature</label> + <input type="text" value="${currentProfile.llm_settings?.temperature || ''}" readonly + class="${STYLES.readonlyInput}"> + </div> + </div> + </div> + `; } -// Work directory functions -function updateWorkDirDisplay() { - const input = document.getElementById('workDirPath'); - if (input && currentProfile) { - input.value = currentProfile.work_dir || ''; - } -} - -async function selectWorkDir() { +// Funciones de estado +async function restoreLastState() { try { - console.log('Requesting directory selection...'); // Debug - const response = await apiRequest('/select-directory'); - console.log('Directory selection response:', response); // Debug - - if (response.path) { - console.log('Updating profile with new work_dir:', response.path); // Debug - const updateResponse = await apiRequest(`/profiles/${currentProfile.id}`, { - method: 'PUT', - body: JSON.stringify({ - ...currentProfile, - work_dir: response.path - }) - }); - console.log('Profile update response:', updateResponse); // Debug - - await selectProfile(currentProfile.id); - showSuccess('Directorio de trabajo actualizado correctamente'); + // Restaurar último grupo seleccionado + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + const groupSelect = document.getElementById('groupSelect'); + if (groupSelect) { + groupSelect.value = lastGroupId; + await handleGroupChange({ target: { value: lastGroupId } }); + } } } catch (error) { - console.error('Error al seleccionar directorio:', error); // Debug - showError('Error al actualizar el directorio de trabajo'); + console.error('Error restoring state:', error); } } -// Output functions +// Funciones de utilidad UI function showError(message) { const output = document.getElementById('outputArea'); + if (!output) return; + const timestamp = new Date().toLocaleTimeString(); output.innerHTML += `\n[${timestamp}] ERROR: ${message}`; output.scrollTop = output.scrollHeight; @@ -233,6 +168,8 @@ function showError(message) { function showSuccess(message) { const output = document.getElementById('outputArea'); + if (!output) return; + const timestamp = new Date().toLocaleTimeString(); output.innerHTML += `\n[${timestamp}] SUCCESS: ${message}`; output.scrollTop = output.scrollHeight; @@ -240,10 +177,32 @@ function showSuccess(message) { function clearOutput() { const output = document.getElementById('outputArea'); - output.innerHTML = ''; + if (output) { + output.innerHTML = ''; + } } -// Modal helper functions +// Event Handlers +async function handleProfileChange(event) { + const profileId = event.target.value; + if (profileId) { + await selectProfile(profileId); + } +} + +async function handleGroupChange(event) { + const groupId = event.target.value; + if (groupId) { + localStorage.setItem('lastGroupId', groupId); + await selectGroup(groupId); + } else { + localStorage.removeItem('lastGroupId'); + currentGroup = null; + updateGroupDisplay(); + } +} + +// Modal Helpers function closeModal(button) { const modal = button.closest('.modal'); if (modal) { @@ -251,14 +210,17 @@ function closeModal(button) { } } -// Global error handler +// Error Handler Global window.addEventListener('unhandledrejection', function(event) { console.error('Unhandled promise rejection:', event.reason); showError('An unexpected error occurred'); }); -// Export functions for use in other modules +// Exportar funciones globales window.showError = showError; window.showSuccess = showSuccess; window.closeModal = closeModal; -window.currentProfile = currentProfile; \ No newline at end of file +window.STYLES = STYLES; + +// Inicializar cuando la página carga +document.addEventListener('DOMContentLoaded', initializeApp); \ No newline at end of file diff --git a/frontend/static/js/profile.js b/frontend/static/js/profile.js index 0a5c615..93ce984 100644 --- a/frontend/static/js/profile.js +++ b/frontend/static/js/profile.js @@ -5,66 +5,125 @@ let editingProfile = null; async function loadProfiles() { try { const response = await apiRequest('/profiles'); - const profiles = Object.values(response); + if (!response || !Object.keys(response).length) { + throw new Error('No profiles available'); + } - // Actualizar el selector manteniendo el valor seleccionado + const profiles = Object.values(response); const select = document.getElementById('profileSelect'); + + // Actualizar el selector select.innerHTML = profiles.map(profile => ` - <option value="${profile.id}"> - ${profile.name} + <option value="${profile.id}" ${profile.id === selectedProfileId ? 'selected' : ''}> + ${profile.name || profile.id} </option> `).join(''); - // Establecer el valor seleccionado después de actualizar las opciones - if (response[selectedProfileId]) { + // Intentar seleccionar el perfil guardado o el predeterminado + const savedProfile = profiles.find(p => p.id === selectedProfileId); + if (savedProfile) { + await selectProfile(savedProfile.id); + } else { + // Si no se encuentra el perfil guardado, usar el primero disponible + selectedProfileId = profiles[0].id; select.value = selectedProfileId; await selectProfile(selectedProfileId); - } else { - selectedProfileId = 'default'; - select.value = 'default'; - await selectProfile('default'); } - // Asegurarse de que el evento change no sobrescriba la selección - select.addEventListener('change', onProfileChange, { once: true }); + localStorage.setItem('selectedProfileId', selectedProfileId); } catch (error) { - showError('Error al cargar los perfiles'); + console.error('Error loading profiles:', error); + showError('Error loading profiles. Using default profile.'); + await loadDefaultProfile(); + } +} + +async function loadDefaultProfile() { + try { + currentProfile = await apiRequest('/profiles/default'); + selectedProfileId = 'default'; + localStorage.setItem('selectedProfileId', 'default'); + + const select = document.getElementById('profileSelect'); + select.innerHTML = `<option value="default">Default Profile</option>`; + select.value = 'default'; + + // Actualizar la visualización del perfil + updateProfileDisplay(); + + } catch (error) { + console.error('Error loading default profile:', error); + showError('Failed to load default profile'); } } async function selectProfile(profileId) { try { - currentProfile = await apiRequest(`/profiles/${profileId}`); - updateWorkDirDisplay(); + const response = await apiRequest(`/profiles/${profileId}`); + if (!response || response.error) { + throw new Error(response?.error || 'Profile not found'); + } + + currentProfile = response; + selectedProfileId = profileId; + localStorage.setItem('selectedProfileId', profileId); + + // Actualizar la visualización del perfil + updateProfileDisplay(); + } catch (error) { - showError('Failed to load profile'); + console.error('Failed to load profile:', error); + showError(`Failed to load profile: ${error.message}`); + // Intentar cargar el perfil por defecto si falla + if (profileId !== 'default') { + await loadDefaultProfile(); + } } } +function updateProfileDisplay() { + const profileConfig = document.getElementById('profileConfig'); + if (!profileConfig || !currentProfile) return; + + profileConfig.innerHTML = ` + <div class="space-y-4"> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-500">Profile ID</label> + <div class="mt-1 text-sm">${currentProfile.id}</div> + </div> + <div> + <label class="block text-sm font-medium text-gray-500">Name</label> + <div class="mt-1 text-sm">${currentProfile.name}</div> + </div> + </div> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-500">LLM Model</label> + <div class="mt-1 text-sm">${currentProfile.llm_settings?.model || 'Not set'}</div> + </div> + <div> + <label class="block text-sm font-medium text-gray-500">Temperature</label> + <div class="mt-1 text-sm">${currentProfile.llm_settings?.temperature || 'Not set'}</div> + </div> + </div> + <div> + <label class="block text-sm font-medium text-gray-500">API Key</label> + <div class="mt-1 text-sm"> + ${currentProfile.llm_settings?.api_key ? '********' : 'Not set'} + </div> + </div> + </div> + `; +} + async function changeProfile() { const select = document.getElementById('profileSelect'); await selectProfile(select.value); } -async function selectWorkDir() { - try { - const response = await apiRequest('/select-directory'); - if (response.path) { - await apiRequest(`/profiles/${currentProfile.id}`, { - method: 'PUT', - body: JSON.stringify({ - ...currentProfile, - work_dir: response.path - }) - }); - await selectProfile(currentProfile.id); - showSuccess('Work directory updated successfully'); - } - } catch (error) { - showError('Failed to update work directory'); - } -} +// Eliminar la función updateWorkDirDisplay y selectWorkDir // Profile editor modal @@ -95,12 +154,6 @@ function showProfileEditor(profile = null) { class="${editableInputClass}" value="${profile?.name || ''}" required> </div> - <div class="form-group"> - <label for="workDir" class="block text-sm font-medium text-gray-700">Directorio de Trabajo</label> - <input type="text" id="workDir" name="work_dir" - class="${readonlyInputClass}" - value="${profile?.work_dir || ''}" readonly> - </div> <div class="form-group"> <label for="llmModel" class="block text-sm font-medium text-gray-700">LLM Model</label> <select id="llmModel" name="llm_model" @@ -143,7 +196,6 @@ async function saveProfile(event) { const profileData = { id: formData.get('id'), name: formData.get('name'), - work_dir: formData.get('work_dir'), llm_settings: { model: formData.get('llm_model'), api_key: formData.get('api_key'), @@ -192,11 +244,6 @@ async function editProfile() { <input type="text" name="name" value="${currentProfile.name}" class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> </div> - <div> - <label class="block text-sm font-medium text-gray-700">Work Directory</label> - <input type="text" name="work_dir" value="${currentProfile.work_dir}" readonly - class="mt-1 block w-full rounded-md border-2 border-gray-300 bg-green-50 px-3 py-2 shadow-sm"> - </div> <div> <label class="block text-sm font-medium text-gray-700">LLM Model</label> <select name="llm_model" @@ -232,7 +279,6 @@ async function saveProfile(modal) { const profileData = { id: formData.get('id'), name: formData.get('name'), - work_dir: formData.get('work_dir'), llm_settings: { model: formData.get('llm_model'), api_key: formData.get('api_key'), @@ -278,11 +324,6 @@ function newProfile() { <input type="text" name="name" required class="${editableInputClass}"> </div> - <div> - <label class="block text-sm font-medium text-gray-700">Work Directory</label> - <input type="text" name="work_dir" readonly - class="${readonlyInputClass}"> - </div> <div> <label class="block text-sm font-medium text-gray-700">LLM Model</label> <select name="llm_model" diff --git a/frontend/static/js/scripts.js b/frontend/static/js/scripts.js index 4f85782..5fdef57 100644 --- a/frontend/static/js/scripts.js +++ b/frontend/static/js/scripts.js @@ -1,35 +1,20 @@ // frontend/static/js/scripts.js -// Script groups state -let scriptGroups = []; - -// Load script groups when page loads -document.addEventListener('DOMContentLoaded', async () => { - await loadScriptGroups(); -}); - -// Load script groups when page loads -document.addEventListener('DOMContentLoaded', async () => { - await loadScriptGroups(); -}); - async function loadScriptGroups() { try { - // Obtener los grupos desde el servidor const groups = await apiRequest('/script-groups'); - console.log('Loaded script groups:', groups); - - // Obtener el selector y el último grupo seleccionado const select = document.getElementById('groupSelect'); const lastGroupId = localStorage.getItem('lastGroupId'); - console.log('Last group ID:', lastGroupId); - + // Remover event listener anterior si existe - select.removeEventListener('change', handleGroupChange); + const oldHandler = select.onchange; + if (oldHandler) { + select.removeEventListener('change', oldHandler); + } - // Construir las opciones + // Actualizar opciones select.innerHTML = ` - <option value="">Seleccionar grupo...</option> + <option value="">Select group...</option> ${groups.map(group => ` <option value="${group.id}" ${group.id === lastGroupId ? 'selected' : ''}> ${group.name} @@ -37,779 +22,418 @@ async function loadScriptGroups() { `).join('')} `; - // Agregar event listener para cambios + // Agregar nuevo event listener select.addEventListener('change', handleGroupChange); - console.log('Added change event listener to groupSelect'); // Si hay un grupo guardado, cargarlo - if (lastGroupId) { - console.log('Loading last group scripts:', lastGroupId); - await loadGroupScripts(lastGroupId); + if (lastGroupId && groups.some(g => g.id === lastGroupId)) { + await selectGroup(lastGroupId); } - } catch (error) { - console.error('Error al cargar grupos de scripts:', error); - showError('Error al cargar grupos de scripts'); + console.error('Error loading script groups:', error); + showError('Error loading script groups'); } } - -// Función para manejar el cambio de grupo async function handleGroupChange(event) { const groupId = event.target.value; - console.log('Group selection changed:', groupId); - if (groupId) { localStorage.setItem('lastGroupId', groupId); - console.log('Saved lastGroupId:', groupId); + await selectGroup(groupId); } else { localStorage.removeItem('lastGroupId'); - console.log('Removed lastGroupId'); + updateGroupDisplay(null); } - - await loadGroupScripts(groupId); } -// Actualizar función de cambio de perfil para mantener la persistencia -async function changeProfile() { - const select = document.getElementById('profileSelect'); - if (select.value) { - await selectProfile(select.value); - localStorage.setItem('lastProfileId', select.value); +async function selectGroup(groupId) { + try { + // Cargar configuración del grupo + const config = await apiRequest(`/script-groups/${groupId}/config`); + currentGroup = { id: groupId, ...config }; - // Al cambiar de perfil, intentamos mantener el último grupo seleccionado - const lastGroupId = localStorage.getItem('lastGroupId'); - if (lastGroupId) { - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect) { - groupSelect.value = lastGroupId; - await loadGroupScripts(lastGroupId); - } + // Actualizar displays + updateGroupDisplay(currentGroup); + + // Cargar scripts si hay un directorio de trabajo configurado + if (currentGroup.work_dir) { + await loadGroupScripts(groupId); + // Cargar configuración del directorio de trabajo + const workDirConfig = await apiRequest(`/workdir-config/${groupId}`); + updateWorkDirConfig(workDirConfig); + } else { + document.getElementById('scriptList').innerHTML = ` + <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4"> + <div class="flex"> + <div class="ml-3"> + <p class="text-sm text-yellow-700"> + Please configure a work directory for this group first + </p> + </div> + </div> + </div> + `; } + } catch (error) { + console.error('Error selecting group:', error); + showError('Error loading group configuration'); } } async function loadGroupScripts(groupId) { - const scriptList = document.getElementById('scriptList'); - - if (!groupId) { - scriptList.style.display = 'none'; - localStorage.removeItem('lastGroupId'); // Limpiar selección + if (!currentGroup?.work_dir) { return; } - // Guardar grupo seleccionado - localStorage.setItem('lastGroupId', groupId); - console.log('Group saved:', groupId); + try { + // Cargar scripts y esquema + const [scripts, schema] = await Promise.all([ + apiRequest(`/script-groups/${groupId}/scripts`), + apiRequest(`/script-groups/${groupId}/schema`) + ]); - if (!currentProfile?.work_dir) { + const scriptList = document.getElementById('scriptList'); scriptList.innerHTML = ` - <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4"> - <div class="flex"> - <div class="ml-3"> - <p class="text-sm text-yellow-700"> - Por favor, seleccione primero un directorio de trabajo - </p> + <div class="space-y-4"> + ${scripts.map(script => ` + <div class="bg-white shadow overflow-hidden sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <div class="flex justify-between items-start"> + <div> + <h3 class="text-lg leading-6 font-medium text-gray-900"> + ${script.name} + </h3> + <p class="mt-1 max-w-2xl text-sm text-gray-500"> + ${script.description || 'No description available'} + </p> + </div> + <button onclick="runScript('${script.id}')" + class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> + Run + </button> + </div> + </div> </div> - </div> + `).join('')} </div> `; - scriptList.style.display = 'block'; - return; - } - - try { - console.log('Loading data for group:', groupId); - - // Actualizar el selector para reflejar la selección actual - const groupSelect = document.getElementById('groupSelect'); - if (groupSelect && groupSelect.value !== groupId) { - groupSelect.value = groupId; - } - - // Cargar y loguear scripts - let groupScripts, configSchema; - try { - groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); - console.log('Scripts loaded:', groupScripts); - } catch (e) { - console.error('Error loading scripts:', e); - throw e; - } - - try { - configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); - console.log('Config schema loaded:', configSchema); - } catch (e) { - console.error('Error loading config schema:', e); - throw e; - } - - // Intentar cargar configuración actual - let currentConfig = {}; - try { - currentConfig = await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - console.log('Current config loaded:', currentConfig); - } catch (e) { - console.warn('No existing configuration found, using defaults'); - } - - // Verificar que tenemos los datos necesarios - if (!groupScripts || !configSchema) { - throw new Error('Failed to load required data'); - } - - console.log('Rendering UI with:', { - groupScripts, - configSchema, - currentConfig - }); - - scriptList.innerHTML = ` - <!-- Sección de Configuración --> - <div class="mb-6 bg-white shadow sm:rounded-lg"> - <div class="border-b border-gray-200 p-4 flex justify-between items-center"> - <div> - <h3 class="text-lg font-medium text-gray-900"> - ${configSchema.group_name || 'Configuración'} - </h3> - <p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p> - </div> - <button onclick="editConfigSchema('${groupId}')" - class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> - </svg> - Editar Esquema - </button> - </div> - <div class="p-4"> - <form id="groupConfigForm" class="grid grid-cols-2 gap-4"> - ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` - <div class="space-y-2 col-span-2"> - <label class="block text-sm font-medium text-gray-700"> - ${field.description} - </label> - ${generateFormField(key, field, currentConfig[key])} - </div> - `).join('')} - <div class="col-span-2 flex justify-end pt-4"> - <button type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Configuración - </button> - </div> - </form> - </div> - </div> - - <!-- Lista de Scripts --> - <div class="space-y-4"> - ${groupScripts.map(script => ` - <div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg"> - <div class="flex justify-between items-start"> - <div> - <h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4> - <p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p> - </div> - <button onclick="runScript('${groupId}', '${script.id}')" - class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Ejecutar - </button> - </div> - </div> - `).join('')} - </div>`; - - scriptList.style.display = 'block'; - - // Agregar evento para guardar configuración - const form = document.getElementById('groupConfigForm'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - await saveGroupConfig(groupId, form); - }); - } catch (error) { - console.error('Error in loadGroupScripts:', error); - showError('Failed to load scripts and configuration'); + console.error('Error loading group scripts:', error); + showError('Error loading scripts'); } } -// Update script groups display -function updateScriptGroupsDisplay() { - const container = document.getElementById('scriptGroups'); - - if (!scriptGroups.length) { - container.innerHTML = '<p class="no-scripts">No script groups available</p>'; +async function runScript(scriptId) { + if (!currentGroup?.work_dir) { + showError('No work directory configured'); return; } - - container.innerHTML = scriptGroups.map(group => ` - <div class="script-group" data-group-id="${group.id}"> - <div class="script-group-header"> - <h3>${group.name}</h3> - <button onclick="configureGroup('${group.id}')" class="config-btn"> - Configure - </button> - </div> - <div class="script-list"> - ${group.scripts.map(script => ` - <div class="script-item"> - <div class="script-info"> - <h4>${script.name}</h4> - <p>${script.description || 'No description available'}</p> - </div> - <div class="script-actions"> - <button onclick="runScript('${group.id}', '${script.id}')" class="run-btn"> - Run - </button> - </div> - </div> - `).join('')} - </div> - </div> - `).join(''); -} -// Run a script -async function runScript(groupId, scriptId) { - if (!currentProfile?.work_dir) { - showError('Please select a work directory first'); - return; - } - try { - const result = await apiRequest(`/scripts/${groupId}/${scriptId}/run`, { + const result = await apiRequest(`/script-groups/${currentGroup.id}/scripts/${scriptId}/run`, { method: 'POST', body: JSON.stringify({ - work_dir: currentProfile.work_dir, + work_dir: currentGroup.work_dir, profile: currentProfile }) }); - - if (result.status === 'error') { + + if (result.error) { showError(result.error); } else { - showSuccess(`Script ${scriptId} executed successfully`); + showSuccess('Script executed successfully'); if (result.output) { - const output = document.getElementById('outputArea'); - output.innerHTML += `\n[${new Date().toLocaleTimeString()}] ${result.output}`; - output.scrollTop = output.scrollHeight; + const outputArea = document.getElementById('outputArea'); + const timestamp = new Date().toLocaleTimeString(); + outputArea.innerHTML += `\n[${timestamp}] ${result.output}`; + outputArea.scrollTop = outputArea.scrollHeight; } } } catch (error) { - showError(`Failed to run script: ${error.message}`); + showError('Error executing script'); } } -// Configure script group -async function configureGroup(groupId) { - if (!currentProfile?.work_dir) { - showError('Please select a work directory first'); - return; - } - +async function editGroupConfig() { + if (!currentGroup) return; + try { - const config = await getGroupConfig(groupId); - showGroupConfigEditor(groupId, config); - } catch (error) { - showError('Failed to load group configuration'); - } -} - -// Show group configuration editor -function showGroupConfigEditor(groupId, config) { - const group = scriptGroups.find(g => g.id === groupId); - if (!group) return; - - const modal = document.createElement('div'); - modal.className = 'modal active'; - - modal.innerHTML = ` - <div class="modal-content"> - <h2>${group.name} Configuration</h2> - <form id="groupConfigForm" onsubmit="saveGroupConfig(event, '${groupId}')"> - <div class="form-group"> - <label for="configData">Configuration (JSON)</label> - <textarea id="configData" name="config" rows="10" - class="config-editor">${JSON.stringify(config || {}, null, 2)}</textarea> - </div> - <div class="button-group"> - <button type="button" onclick="closeModal(this)">Cancel</button> - <button type="submit">Save</button> - </div> - </form> - </div> - `; - - document.body.appendChild(modal); -} - -function generateFormField(key, field, currentValue) { - switch (field.type) { - case 'string': - return ` - <input type="text" - name="${key}" - value="${currentValue || ''}" - class="${STYLES.editableInput}"> - `; - case 'number': - return ` - <input type="number" - name="${key}" - value="${currentValue || 0}" - class="${STYLES.editableInput}"> - `; - case 'boolean': - return ` - <select name="${key}" class="${STYLES.editableInput}"> - <option value="true" ${currentValue ? 'selected' : ''}>Yes</option> - <option value="false" ${!currentValue ? 'selected' : ''}>No</option> - </select> - `; - case 'select': - return ` - <select name="${key}" class="${STYLES.editableInput}"> - ${field.options.map(opt => ` - <option value="${opt}" ${currentValue === opt ? 'selected' : ''}> - ${opt} - </option> - `).join('')} - </select> - `; - default: - return `<input type="text" name="${key}" value="${currentValue || ''}" class="${STYLES.editableInput}">`; - } -} - -async function loadGroupScripts(groupId) { - const scriptList = document.getElementById('scriptList'); - - if (!groupId) { - scriptList.style.display = 'none'; - localStorage.removeItem('lastGroupId'); - return; - } - - if (!currentProfile?.work_dir) { - scriptList.innerHTML = ` - <div class="bg-yellow-50 border-l-4 border-yellow-400 p-4"> - <div class="flex"> - <div class="ml-3"> - <p class="text-sm text-yellow-700"> - Por favor, seleccione primero un directorio de trabajo - </p> - </div> - </div> - </div> - `; - scriptList.style.display = 'block'; - return; - } - - try { - console.log('Loading data for group:', groupId); - - // Cargar y loguear scripts - let groupScripts, configSchema; - try { - groupScripts = await apiRequest(`/script-groups/${groupId}/scripts`); - console.log('Scripts loaded:', groupScripts); - } catch (e) { - console.error('Error loading scripts:', e); - throw e; - } - - try { - configSchema = await apiRequest(`/script-groups/${groupId}/config-schema`); - console.log('Config schema loaded:', configSchema); - } catch (e) { - console.error('Error loading config schema:', e); - throw e; - } - - // Intentar cargar configuración actual - let currentConfig = {}; - try { - console.log('Loading current config for work_dir:', currentProfile.work_dir); - currentConfig = await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - console.log('Current config loaded:', currentConfig); - } catch (e) { - console.warn('No existing configuration found, using defaults'); - } - - // Verificar que tenemos los datos necesarios - if (!groupScripts || !configSchema) { - throw new Error('Failed to load required data'); - } - - console.log('Rendering UI with:', { - groupScripts, - configSchema, - currentConfig - }); - - scriptList.innerHTML = ` - <!-- Sección de Configuración --> - <div class="mb-6 bg-white shadow sm:rounded-lg"> - <div class="border-b border-gray-200 p-4 flex justify-between items-center"> + const schema = await apiRequest(`/script-groups/${currentGroup.id}/schema`); + const content = ` + <form id="groupConfigForm" class="space-y-4"> + ${Object.entries(schema.config_schema).map(([key, field]) => ` <div> - <h3 class="text-lg font-medium text-gray-900"> - ${configSchema.group_name || 'Configuración'} - </h3> - <p class="mt-1 text-sm text-gray-500">${configSchema.description || ''}</p> - </div> - <button onclick="editConfigSchema('${groupId}')" - class="rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-900 hover:bg-gray-200 flex items-center gap-2"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/> - </svg> - Editar Esquema - </button> - </div> - <div class="p-4"> - <form id="groupConfigForm" class="grid grid-cols-2 gap-4"> - ${Object.entries(configSchema.config_schema || {}).map(([key, field]) => ` - <div class="space-y-2 col-span-2"> - <label class="block text-sm font-medium text-gray-700"> - ${field.description} - </label> - ${generateFormField(key, field, currentConfig[key])} - </div> - `).join('')} - <div class="col-span-2 flex justify-end pt-4"> - <button type="submit" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Configuración - </button> - </div> - </form> - </div> - </div> - - <!-- Lista de Scripts --> - <div class="space-y-4"> - ${groupScripts.map(script => ` - <div class="bg-white px-4 py-3 rounded-md border border-gray-200 hover:border-gray-300 shadow sm:rounded-lg"> - <div class="flex justify-between items-start"> - <div> - <h4 class="text-sm font-medium text-gray-900">${script.name || script.id}</h4> - <p class="mt-1 text-sm text-gray-500">${script.description || 'Sin descripción disponible'}</p> - </div> - <button onclick="runScript('${groupId}', '${script.id}')" - class="ml-4 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Ejecutar - </button> - </div> + <label class="block text-sm font-medium text-gray-700"> + ${field.description || key} + </label> + ${generateFormField(key, field, currentGroup[key])} </div> `).join('')} - </div>`; - - scriptList.style.display = 'block'; - - // Agregar evento para guardar configuración - const form = document.getElementById('groupConfigForm'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - await saveGroupConfig(groupId, form); - }); - - } catch (error) { - console.error('Error in loadGroupScripts:', error); - showError('Failed to load scripts and configuration'); - } -} - -async function editConfigSchema(groupId) { - try { - const schema = await apiRequest(`/script-groups/${groupId}/config-schema`); - const configSection = document.createElement('div'); - configSection.id = 'schemaEditor'; - configSection.className = 'mb-6 bg-white shadow sm:rounded-lg'; - - configSection.innerHTML = ` - <div class="border-b border-gray-200 p-4 flex justify-between items-center bg-gray-50"> - <h3 class="text-lg font-medium text-gray-900">Editar Configuración del Esquema</h3> - <button onclick="this.closest('#schemaEditor').remove()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cerrar Editor - </button> - </div> - <div class="p-4"> - <div class="space-y-4"> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Nombre del Grupo</label> - <input type="text" name="group_name" value="${schema.group_name}" - class="${STYLES.editableInput}"> - </div> - <div class="text-right"> - <button onclick="addParameter(this)" - class="mt-6 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500"> - Agregar Parámetro - </button> - </div> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Descripción</label> - <input type="text" name="description" value="${schema.description}" - class="${STYLES.editableInput}"> - </div> - <div id="parameters" class="space-y-2"> - <div class="flex justify-between items-center"> - <h4 class="font-medium text-gray-900">Parámetros</h4> - </div> - ${Object.entries(schema.config_schema).map(([key, param]) => ` - <div class="parameter-item bg-gray-50 p-4 rounded-md relative"> - <button onclick="removeParameter(this)" - class="absolute top-2 right-2 text-red-600 hover:text-red-700"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> - </svg> - </button> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Parameter Name</label> - <input type="text" name="param_name" value="${key}" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Type</label> - <select name="param_type" - onchange="handleTypeChange(this)" - class="${STYLES.editableInput}"> - <option value="string" ${param.type === 'string' ? 'selected' : ''}>String</option> - <option value="number" ${param.type === 'number' ? 'selected' : ''}>Number</option> - <option value="boolean" ${param.type === 'boolean' ? 'selected' : ''}>Boolean</option> - <option value="select" ${param.type === 'select' ? 'selected' : ''}>Select</option> - </select> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Description</label> - <input type="text" name="param_description" value="${param.description}" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Default Value</label> - <input type="text" name="param_default" value="${param.default}" - class="${STYLES.editableInput}"> - </div> - ${param.type === 'select' ? ` - <div> - <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> - <input type="text" name="param_options" value="${param.options.join(', ')}" - class="${STYLES.editableInput}"> - </div> - ` : ''} - </div> - </div> - `).join('')} - </div> - <div class="flex justify-end space-x-3 pt-4 border-t border-gray-200"> - <button onclick="this.closest('#schemaEditor').remove()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cancelar - </button> - <button onclick="saveConfigSchema('${groupId}', this.closest('#schemaEditor'))" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar Cambios - </button> - </div> - </div> - </div> + </form> `; - // Insertamos el editor justo después del botón "Edit Schema" - const scriptList = document.getElementById('scriptList'); - const existingEditor = document.getElementById('schemaEditor'); - if (existingEditor) { - existingEditor.remove(); - } - scriptList.insertBefore(configSection, scriptList.firstChild); - + const modal = createModal('Edit Group Configuration', content, true); + modal.querySelector('[onclick="saveModal(this)"]').onclick = () => saveGroupConfig(modal); } catch (error) { - showError('Error al cargar el esquema de configuración'); + showError('Error loading group configuration'); } } -function createModal(title, content, onSave = null) { - const modal = document.createElement('div'); - modal.className = 'fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4'; - - modal.innerHTML = ` - <div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col"> - <div class="px-6 py-4 border-b border-gray-200"> - <h3 class="text-lg font-medium text-gray-900">${title}</h3> - </div> - <div class="px-6 py-4 overflow-y-auto flex-1"> - ${content} - </div> - <div class="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end gap-3 border-t border-gray-200"> - <button onclick="closeModal(this)" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Cancelar - </button> - ${onSave ? ` - <button onclick="saveModal(this)" - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - Guardar - </button> - ` : ''} - </div> - </div> - `; +async function saveGroupConfig(modal) { + if (!currentGroup) return; - document.body.appendChild(modal); - return modal; -} + const form = modal.querySelector('#groupConfigForm'); + const formData = new FormData(form); + const config = {}; -function addParameter(button) { - const parametersDiv = button.closest('.space-y-4').querySelector('#parameters'); - const newParam = document.createElement('div'); - newParam.className = 'parameter-item bg-gray-50 p-4 rounded-md relative'; - newParam.innerHTML = ` - <button onclick="removeParameter(this)" - class="absolute top-2 right-2 text-red-600 hover:text-red-700"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> - </svg> - </button> - <div class="grid grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-700">Parameter Name</label> - <input type="text" name="param_name" value="" - class="${STYLES.editableInput}"> - </div> - <div> - <label class="block text-sm font-medium text-gray-700">Type</label> - <select name="param_type" - onchange="handleTypeChange(this)" - class="${STYLES.editableInput}"> - <option value="string">String</option> - <option value="number">Number</option> - <option value="boolean">Boolean</option> - <option value="select">Select</option> - </select> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Description</label> - <input type="text" name="param_description" value="" - class="${STYLES.editableInput}"> - </div> - <div class="col-span-2"> - <label class="block text-sm font-medium text-gray-700">Default Value</label> - <input type="text" name="param_default" value="" - class="${STYLES.editableInput}"> - </div> - </div> - `; - parametersDiv.appendChild(newParam); -} - -function removeParameter(button) { - button.closest('.parameter-item').remove(); -} - -function handleTypeChange(select) { - const paramItem = select.closest('.parameter-item'); - const optionsDiv = paramItem.querySelector('[name="param_options"]')?.closest('div'); - - if (select.value === 'select') { - if (!optionsDiv) { - const div = document.createElement('div'); - div.className = 'col-span-2'; - div.innerHTML = ` - <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> - <input type="text" name="param_options" value="" - class="${STYLES.editableInput}"> - `; - paramItem.querySelector('.grid').appendChild(div); - } - } else { - optionsDiv?.remove(); - } -} - -async function saveConfigSchema(groupId, modal) { - const form = modal.querySelector('div'); - const schema = { - group_name: form.querySelector('[name="group_name"]').value, - description: form.querySelector('[name="description"]').value, - config_schema: {} - }; - - // Recopilar parámetros - form.querySelectorAll('.parameter-item').forEach(item => { - const name = item.querySelector('[name="param_name"]').value; - const type = item.querySelector('[name="param_type"]').value; - const description = item.querySelector('[name="param_description"]').value; - const defaultValue = item.querySelector('[name="param_default"]').value; - - const param = { - type, - description, - default: type === 'boolean' ? defaultValue === 'true' : defaultValue - }; - - if (type === 'select') { - const options = item.querySelector('[name="param_options"]').value - .split(',') - .map(opt => opt.trim()) - .filter(Boolean); - param.options = options; - } - - schema.config_schema[name] = param; + formData.forEach((value, key) => { + if (value === 'true') value = true; + else if (value === 'false') value = false; + else if (!isNaN(value) && value !== '') value = Number(value); + config[key] = value; }); try { - await apiRequest(`/script-groups/${groupId}/config-schema`, { + await apiRequest(`/script-groups/${currentGroup.id}/config`, { + method: 'PUT', + body: JSON.stringify(config) + }); + + closeModal(modal); + showSuccess('Group configuration updated'); + await selectGroup(currentGroup.id); + } catch (error) { + showError('Error saving group configuration'); + } +} + +async function editGroupSchema() { + if (!currentGroup) return; + + try { + const schema = await apiRequest(`/script-groups/${currentGroup.id}/schema`); + + const content = ` + <div class="space-y-4"> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-700">Group Name</label> + <input type="text" name="group_name" value="${schema.group_name || ''}" + class="${STYLES.editableInput}"> + </div> + <div> + <div class="flex justify-end"> + <button type="button" onclick="addSchemaField()" + class="mt-6 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500"> + Add Field + </button> + </div> + </div> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Description</label> + <input type="text" name="description" value="${schema.description || ''}" + class="${STYLES.editableInput}"> + </div> + <div id="schemaFields" class="space-y-4"> + ${Object.entries(schema.config_schema || {}).map(([key, field]) => + generateSchemaField(key, field)).join('')} + </div> + </div> + `; + + const modal = createModal('Edit Group Schema', content, true); + modal.querySelector('[onclick="saveModal(this)"]').onclick = () => saveGroupSchema(modal); + } catch (error) { + showError('Error loading group schema'); + } +} + +async function saveGroupSchema(modal) { + const schema = { + group_name: modal.querySelector('[name="group_name"]').value, + description: modal.querySelector('[name="description"]').value, + config_schema: {} + }; + + // Recopilar definiciones de campos + modal.querySelectorAll('.schema-field').forEach(field => { + const key = field.querySelector('[name="field_name"]').value; + const type = field.querySelector('[name="field_type"]').value; + + if (!key) return; // Ignorar campos sin nombre + + const fieldSchema = { + type, + description: field.querySelector('[name="field_description"]').value, + required: field.querySelector('[name="field_required"]').value === 'true' + }; + + // Procesar valor por defecto según el tipo + const defaultValue = field.querySelector('[name="field_default"]').value; + if (defaultValue) { + if (type === 'number') { + fieldSchema.default = Number(defaultValue); + } else if (type === 'boolean') { + fieldSchema.default = defaultValue === 'true'; + } else { + fieldSchema.default = defaultValue; + } + } + + // Procesar opciones para campos tipo select + if (type === 'select') { + const optionsStr = field.querySelector('[name="field_options"]').value; + fieldSchema.options = optionsStr.split(',').map(opt => opt.trim()).filter(Boolean); + } + + schema.config_schema[key] = fieldSchema; + }); + + try { + await apiRequest(`/script-groups/${currentGroup.id}/schema`, { method: 'PUT', body: JSON.stringify(schema) }); - closeModal(modal.querySelector('button')); - showSuccess('Configuration schema updated successfully'); - // Recargar la página para mostrar los cambios - loadGroupScripts(groupId); + closeModal(modal); + showSuccess('Group schema updated'); + + // Recargar configuración del grupo + await editGroupConfig(); } catch (error) { - showError('Failed to update configuration schema'); + showError('Error saving group schema'); } } -function showScriptForm(script) { - const modal = document.createElement('div'); - modal.className = 'modal active'; - - modal.innerHTML = ` - <div class="modal-content"> - <h2 class="text-xl font-bold mb-4">${script.name}</h2> - <form id="scriptForm" class="space-y-4"> - <div class="form-group"> - <label class="block text-sm font-medium text-gray-700">Parameters</label> - <textarea name="parameters" rows="4" - class="${STYLES.editableInput}" - placeholder="Enter script parameters (optional)"></textarea> +function updateScriptList(scripts) { + const scriptList = document.getElementById('scriptList'); + if (!scriptList) return; + + if (!scripts || !scripts.length) { + scriptList.innerHTML = ` + <div class="text-center text-gray-500 p-4"> + No scripts available + </div> + `; + return; + } + + scriptList.innerHTML = ` + <div class="space-y-4"> + ${scripts.map(script => ` + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <div class="flex justify-between items-start"> + <div> + <h3 class="text-lg leading-6 font-medium text-gray-900"> + ${script.name} + </h3> + <p class="mt-1 max-w-2xl text-sm text-gray-500"> + ${script.description || 'No description available'} + </p> + </div> + <button onclick="runScript('${script.id}')" + class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> + Run + </button> + </div> + </div> </div> - <div class="mt-4 flex justify-end space-x-3"> - <button type="button" onclick="closeModal(this)" - class="${STYLES.buttonSecondary}">Cancel</button> - <button type="submit" - class="${STYLES.button}">Run</button> + `).join('')} + </div> + `; +} + +async function restoreScriptGroup() { + const lastGroupId = localStorage.getItem('lastGroupId'); + if (lastGroupId) { + const select = document.getElementById('groupSelect'); + if (select) { + select.value = lastGroupId; + if (select.value === lastGroupId) { // Verifica que el grupo aún existe + await selectGroup(lastGroupId); + } else { + localStorage.removeItem('lastGroupId'); + } + } + } +} + +function updateGroupDisplay(group) { + const configContainer = document.getElementById('groupConfig'); + if (!configContainer) return; + + if (!group) { + configContainer.innerHTML = ''; + return; + } + + configContainer.innerHTML = ` + <div class="space-y-4"> + <div class="flex justify-between items-center"> + <div> + <h3 class="text-lg font-medium">${group.name || 'Unnamed Group'}</h3> + <p class="text-sm text-gray-500">${group.description || 'No description'}</p> + </div> + <div class="flex space-x-2"> + <button onclick="editGroupConfig()" + class="px-3 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> + Edit Config + </button> + </div> + </div> + <div class="bg-gray-50 p-4 rounded-md"> + <div class="flex justify-between items-center"> + <div class="flex-grow"> + <label class="block text-sm font-medium text-gray-700">Working Directory</label> + <div class="mt-1 flex items-center space-x-2"> + <span class="text-gray-600">${group.work_dir || 'Not configured'}</span> + </div> + </div> + <button onclick="selectGroupWorkDir()" + class="px-3 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> + ${group.work_dir ? 'Change' : 'Select'} Directory + </button> </div> - </form> - <div id="scriptOutput" class="mt-4 hidden"> - <h3 class="font-bold mb-2">Output:</h3> - <pre class="output-area p-4 bg-gray-100 rounded"></pre> </div> </div> `; - - document.body.appendChild(modal); -} \ No newline at end of file +} + +async function selectGroupWorkDir() { + if (!currentGroup) { + showError('No group selected'); + return; + } + + try { + const response = await apiRequest('/select-directory'); + if (response.path) { + const updatedConfig = { + ...currentGroup, + work_dir: response.path + }; + + await apiRequest(`/script-groups/${currentGroup.id}/config`, { + method: 'PUT', + body: JSON.stringify(updatedConfig) + }); + + currentGroup = updatedConfig; + updateGroupDisplay(currentGroup); + showSuccess('Work directory updated successfully'); + + // Recargar scripts si hay + await loadGroupScripts(currentGroup.id); + } + } catch (error) { + showError('Failed to update work directory'); + } +} + +// Inicializar cuando la página carga +document.addEventListener('DOMContentLoaded', async () => { + await loadScriptGroups(); + await restoreScriptGroup(); +}); \ No newline at end of file diff --git a/frontend/static/js/utils.js b/frontend/static/js/utils.js new file mode 100644 index 0000000..3bc7c33 --- /dev/null +++ b/frontend/static/js/utils.js @@ -0,0 +1,3 @@ +// Eliminar la función updateWorkDirDisplay ya que no se necesita más + +// ...resto de funciones de utilidad... diff --git a/frontend/static/js/workdir_config.js b/frontend/static/js/workdir_config.js index 8d17144..0231243 100644 --- a/frontend/static/js/workdir_config.js +++ b/frontend/static/js/workdir_config.js @@ -1,161 +1,77 @@ // frontend/static/js/workdir_config.js -async function getWorkDirConfig() { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return null; - } - - try { - return await apiRequest(`/workdir-config/${encodeURIComponent(currentProfile.work_dir)}`); - } catch (error) { - showError('Error al cargar la configuración del directorio de trabajo'); - return null; - } -} - -async function getGroupConfig(groupId) { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return null; - } - - try { - return await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}` - ); - } catch (error) { - showError('Error al cargar la configuración del grupo'); - return null; - } -} - -async function updateGroupConfig(groupId, settings) { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); - return false; - } - - try { - await apiRequest( - `/workdir-config/${encodeURIComponent(currentProfile.work_dir)}/group/${groupId}`, - { - method: 'PUT', - body: JSON.stringify(settings) - } - ); - showSuccess('Group configuration updated successfully'); - return true; - } catch (error) { - showError('Failed to update group configuration'); - return false; - } -} - -function showConfigEditor(config, schema) { - const modal = document.createElement('div'); - modal.className = 'modal active'; - - const formContent = Object.entries(schema).map(([key, field]) => ` - <div class="form-group"> - <label class="block text-sm font-medium text-gray-700">${field.description || key}</label> - ${getInputByType(key, field, config[key])} - </div> - `).join(''); - - modal.innerHTML = ` - <div class="modal-content"> - <h2 class="text-xl font-bold mb-4">Work Directory Configuration</h2> - <form id="configForm" class="space-y-4"> - ${formContent} - <div class="mt-4 flex justify-end space-x-3"> - <button type="button" onclick="closeModal(this)" - class="${STYLES.buttonSecondary}">Cancel</button> - <button type="submit" - class="${STYLES.button}">Save</button> - </div> - </form> - </div> - `; - - document.body.appendChild(modal); -} - -function getInputByType(key, field, value) { - switch (field.type) { - case 'select': - return ` - <select name="${key}" - class="${STYLES.editableInput}"> - ${field.options.map(opt => ` - <option value="${opt}" ${value === opt ? 'selected' : ''}> - ${opt} - </option> - `).join('')} - </select>`; - case 'boolean': - return ` - <select name="${key}" - class="${STYLES.editableInput}"> - <option value="true" ${value ? 'selected' : ''}>Yes</option> - <option value="false" ${!value ? 'selected' : ''}>No</option> - </select>`; - case 'number': - return ` - <input type="number" name="${key}" - value="${value || field.default || ''}" - class="${STYLES.editableInput}">`; - default: - return ` - <input type="text" name="${key}" - value="${value || field.default || ''}" - class="${STYLES.editableInput}">`; - } -} - -// static/js/workdir_config.js -async function showWorkDirConfig() { - if (!currentProfile?.work_dir) { - showError('No se ha seleccionado un directorio de trabajo'); +async function editWorkDirConfig() { + if (!currentGroup?.work_dir) { + showError('No work directory configured'); return; } try { - const config = await getWorkDirConfig(); + // Load current configuration + const config = await apiRequest(`/workdir-config/${currentGroup.id}`); + + // Load schema from script group + const schema = await apiRequest(`/script-groups/${currentGroup.id}/schema`); const content = ` - <div class="space-y-4"> - <div> - <h4 class="text-sm font-medium text-gray-900">Directory</h4> - <p class="mt-1 text-sm text-gray-500">${currentProfile.work_dir}</p> - </div> - <div> - <h4 class="text-sm font-medium text-gray-900">Version</h4> - <p class="mt-1 text-sm text-gray-500">${config.version}</p> - </div> - <div> - <h4 class="text-sm font-medium text-gray-900">Group Configurations</h4> - <div class="mt-2 space-y-3"> - ${Object.entries(config.group_settings || {}).map(([groupId, settings]) => ` - <div class="rounded-md bg-gray-50 p-3"> - <h5 class="text-sm font-medium text-gray-900">${groupId}</h5> - <pre class="mt-2 text-xs text-gray-500">${JSON.stringify(settings, null, 2)}</pre> - </div> - `).join('')} + <form id="workDirConfigForm" class="space-y-4"> + ${Object.entries(schema.config_schema).map(([key, field]) => ` + <div> + <label class="block text-sm font-medium text-gray-700"> + ${field.description || key} + </label> + ${generateFormField(key, field, config[key])} </div> - </div> - </div> + `).join('')} + </form> `; - createModal('Work Directory Configuration', content); + const modal = createModal('Edit Work Directory Configuration', content, true); + modal.querySelector('[onclick="saveModal(this)"]').onclick = () => saveWorkDirConfig(modal); } catch (error) { - showError('Error al cargar la configuración del directorio de trabajo'); + showError('Error loading work directory configuration'); } } -function closeModal(button) { - const modal = button.closest('.modal'); - if (modal) { - modal.remove(); +async function saveWorkDirConfig(modal) { + if (!currentGroup?.work_dir) return; + + const form = modal.querySelector('#workDirConfigForm'); + const formData = new FormData(form); + const config = {}; + + formData.forEach((value, key) => { + if (value === 'true') value = true; + else if (value === 'false') value = false; + else if (!isNaN(value) && value !== '') value = Number(value); + config[key] = value; + }); + + try { + await apiRequest(`/workdir-config/${currentGroup.id}`, { + method: 'PUT', + body: JSON.stringify(config) + }); + + closeModal(modal); + showSuccess('Work directory configuration updated'); + + // Reload configuration display + const updatedConfig = await apiRequest(`/workdir-config/${currentGroup.id}`); + updateWorkDirConfig(updatedConfig); + } catch (error) { + showError('Error saving work directory configuration'); } -} \ No newline at end of file +} + +// Initialize configuration when the page loads +document.addEventListener('DOMContentLoaded', async () => { + if (currentGroup?.work_dir) { + try { + const config = await apiRequest(`/workdir-config/${currentGroup.id}`); + updateWorkDirConfig(config); + } catch (error) { + console.error('Error loading initial work directory config:', error); + } + } +}); \ No newline at end of file diff --git a/frontend/templates/base.html b/frontend/templates/base.html index ff6913e..aa43441 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -9,5 +9,14 @@ </head> <body> {% block content %}{% endblock %} + + <!-- Scripts --> + <script src="{{ url_for('static', filename='js/main.js') }}"></script> + <script src="{{ url_for('static', filename='js/modal.js') }}"></script> + <script src="{{ url_for('static', filename='js/profile.js') }}"></script> + <script src="{{ url_for('static', filename='js/scripts.js') }}"></script> + <script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script> + + {% block scripts %}{% endblock %} </body> </html> \ No newline at end of file diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 742a603..715435e 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -1,113 +1,281 @@ -<!DOCTYPE html> -<html lang="en" class="h-full bg-gray-50"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Local Scripts Web</title> - <!-- Tailwind y Alpine.js desde CDN --> - <script src="https://cdn.tailwindcss.com"></script> - <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> - <!-- HeroIcons --> - <script src="https://cdnjs.cloudflare.com/ajax/libs/heroicons/2.0.18/solid/index.min.js"></script> -</head> -<body class="h-full"> - <div class="min-h-full"> - <!-- Navbar --> - <nav class="bg-white shadow-sm"> - <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> - <div class="flex h-16 justify-between"> - <div class="flex"> - <div class="flex flex-shrink-0 items-center"> - <h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1> +{% extends "base.html" %} + +{% block content %} +<div class="min-h-full"> + <!-- Navbar --> + <nav class="bg-white shadow-sm"> + <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> + <div class="flex h-16 justify-between"> + <div class="flex"> + <div class="flex flex-shrink-0 items-center"> + <h1 class="text-xl font-semibold text-gray-900">Local Scripts Web</h1> + </div> + </div> + <div class="flex items-center gap-4"> + <select id="profileSelect" + class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600" + onchange="changeProfile()"> + <option value="">Select Profile</option> + </select> + <button onclick="editProfile()" + class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> + Edit Profile + </button> + <button onclick="newProfile()" + class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> + New Profile + </button> + </div> + </div> + </div> + </nav> + + <!-- Main content --> + <main> + <div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> + <div class="space-y-6"> + <!-- Profile Config Section --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold leading-6 text-gray-900">Profile Configuration</h3> + <div id="profileConfig" class="mt-4"> + <!-- Profile config will be loaded here --> </div> </div> - <div class="flex items-center gap-4"> - <select id="profileSelect" - class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600" - onchange="changeProfile()"> - <option value="">Select Profile</option> + </div> + + <!-- Script Groups Section --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <div class="flex justify-between items-center"> + <h3 class="text-base font-semibold leading-6 text-gray-900">Script Groups</h3> + <button onclick="editGroupSchema()" + class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> + Edit Schema + </button> + </div> + <div class="mt-4 space-y-4"> + <select id="groupSelect" + class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + <option value="">Select Script Group</option> + </select> + + <!-- Group Configuration --> + <div id="groupConfig" class="mt-4"> + <!-- Group config will be loaded here --> + </div> + + <!-- Work Directory Configuration --> + <div id="workDirConfig" class="mt-4"> + <!-- Work dir config will be loaded here --> + </div> + + </div> + </div> + </div> + + <!-- Scripts Section --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3> + <div id="scriptList" class="mt-4 space-y-4"> + <!-- Scripts will be loaded here --> + </div> + </div> + </div> + + <!-- Output Section --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + <div class="flex justify-between items-center"> + <h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3> + <button onclick="clearOutput()" + class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500"> + Clear + </button> + </div> + <div id="outputArea" + class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200"> + </div> + </div> + </div> + </div> + </div> + </main> +</div> +{% endblock %} + +{% block scripts %} +<!-- Load utilities first --> +<script src="{{ url_for('static', filename='js/utils.js') }}"></script> + +<!-- Utility Functions --> +<script> + function generateFormField(key, field, value) { + switch (field.type) { + case 'string': + return ` + <input type="text" name="${key}" + value="${value || field.default || ''}" + class="${STYLES.editableInput}"> + `; + case 'number': + return ` + <input type="number" name="${key}" + value="${value || field.default || 0}" + class="${STYLES.editableInput}"> + `; + case 'boolean': + return ` + <select name="${key}" class="${STYLES.editableInput}"> + <option value="true" ${value ? 'selected' : ''}>Yes</option> + <option value="false" ${!value ? 'selected' : ''}>No</option> + </select> + `; + case 'select': + return ` + <select name="${key}" class="${STYLES.editableInput}"> + ${field.options.map(opt => ` + <option value="${opt}" ${value === opt ? 'selected' : ''}> + ${opt} + </option> + `).join('')} + </select> + `; + case 'directory': + return ` + <div class="flex gap-2"> + <input type="text" name="${key}" + value="${value || field.default || ''}" + readonly + class="${STYLES.readonlyInput}"> + <button type="button" + onclick="selectDirectory('${key}')" + class="${STYLES.buttonSecondary}"> + Browse + </button> + </div> + `; + default: + return ` + <input type="text" name="${key}" + value="${value || field.default || ''}" + class="${STYLES.editableInput}"> + `; + } + } + + function generateSchemaField(key = '', field = {}) { + const fieldId = `field_${Math.random().toString(36).substr(2, 9)}`; + return ` + <div class="schema-field bg-gray-50 p-4 rounded-md relative" id="${fieldId}"> + <button type="button" + onclick="removeSchemaField('${fieldId}')" + class="absolute top-2 right-2 text-red-600 hover:text-red-700"> + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" + d="M6 18L18 6M6 6l12 12"/> + </svg> + </button> + <div class="grid grid-cols-2 gap-4"> + <div> + <label class="block text-sm font-medium text-gray-700">Field Name</label> + <input type="text" name="field_name" + value="${key}" + class="${STYLES.editableInput}"> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Type</label> + <select name="field_type" + onchange="handleFieldTypeChange(this)" + class="${STYLES.editableInput}"> + <option value="string" ${field.type === 'string' ? 'selected' : ''}>String</option> + <option value="number" ${field.type === 'number' ? 'selected' : ''}>Number</option> + <option value="boolean" ${field.type === 'boolean' ? 'selected' : ''}>Boolean</option> + <option value="select" ${field.type === 'select' ? 'selected' : ''}>Select</option> + <option value="directory" ${field.type === 'directory' ? 'selected' : ''}>Directory</option> </select> - <button onclick="editProfile()" - class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Edit Profile - </button> - <button onclick="newProfile()" - class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> - New Profile - </button> </div> + <div class="col-span-2"> + <label class="block text-sm font-medium text-gray-700">Description</label> + <input type="text" name="field_description" + value="${field.description || ''}" + class="${STYLES.editableInput}"> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Default Value</label> + <input type="text" name="field_default" + value="${field.default || ''}" + class="${STYLES.editableInput}"> + </div> + <div> + <label class="block text-sm font-medium text-gray-700">Required</label> + <select name="field_required" class="${STYLES.editableInput}"> + <option value="true" ${field.required ? 'selected' : ''}>Yes</option> + <option value="false" ${!field.required ? 'selected' : ''}>No</option> + </select> + </div> + ${field.type === 'select' ? ` + <div class="col-span-2"> + <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> + <input type="text" name="field_options" + value="${(field.options || []).join(', ')}" + class="${STYLES.editableInput}"> + </div> + ` : ''} </div> </div> - </nav> + `; + } - <!-- Main content --> - <main> - <div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> - <div class="space-y-6"> - <!-- Work Directory Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Work Directory</h3> - <div class="mt-4 flex gap-4"> - <input type="text" id="workDirPath" readonly - class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <button onclick="selectWorkDir()" - class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> - Browse - </button> - </div> - </div> - </div> + function handleFieldTypeChange(select) { + const fieldDiv = select.closest('.schema-field'); + const optionsDiv = fieldDiv.querySelector('[name="field_options"]')?.closest('.col-span-2'); + + if (select.value === 'select' && !optionsDiv) { + const div = document.createElement('div'); + div.className = 'col-span-2'; + div.innerHTML = ` + <label class="block text-sm font-medium text-gray-700">Options (comma-separated)</label> + <input type="text" name="field_options" class="${STYLES.editableInput}"> + `; + fieldDiv.querySelector('.grid').appendChild(div); + } else if (select.value !== 'select' && optionsDiv) { + optionsDiv.remove(); + } + } - <!-- Scripts Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Scripts</h3> - <div class="mt-4 space-y-4"> - <select id="groupSelect" - class="w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> - <option value="">Select Script Group</option> - </select> - <div id="scriptList" class="hidden space-y-4"> - <!-- Scripts will be loaded here --> - </div> - </div> - </div> - </div> + function addSchemaField() { + const container = document.getElementById('schemaFields'); + const field = document.createElement('div'); + field.innerHTML = generateSchemaField(); + container.appendChild(field.firstElementChild); + } - <!-- Output Section --> - <div class="bg-white shadow sm:rounded-lg"> - <div class="px-4 py-5 sm:p-6"> - <div class="flex justify-between items-center"> - <h3 class="text-base font-semibold leading-6 text-gray-900">Output</h3> - <button onclick="clearOutput()" - class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500"> - Clear - </button> - </div> - <div id="outputArea" - class="mt-4 h-64 overflow-y-auto p-4 font-mono text-sm bg-gray-50 rounded-md border border-gray-200"> - </div> - </div> - </div> - </div> - </div> - </main> - </div> + function removeSchemaField(fieldId) { + document.getElementById(fieldId).remove(); + } - <!-- Scripts --> - <script src="{{ url_for('static', filename='js/main.js') }}"></script> - <script src="{{ url_for('static', filename='js/workdir_config.js') }}"></script> - <script src="{{ url_for('static', filename='js/profile.js') }}"></script> - <script src="{{ url_for('static', filename='js/scripts.js') }}"></script> - <script src="{{ url_for('static', filename='js/modal.js') }}"></script> - <!-- Al final del body --> - <script> - // Initialización cuando la página carga - document.addEventListener('DOMContentLoaded', async () => { - console.log('DOM loaded, initializing...'); - await initializeApp(); - }); - </script> -</body> -</html> \ No newline at end of file + async function selectDirectory(inputName) { + try { + const response = await apiRequest('/select-directory'); + if (response.path) { + const input = document.querySelector(`input[name="${inputName}"]`); + if (input) { + input.value = response.path; + } + } + } catch (error) { + showError('Error selecting directory'); + } + } +</script> + +<!-- Initialize app --> +<script> + document.addEventListener('DOMContentLoaded', async () => { + console.log('DOM loaded, initializing...'); + await initializeApp(); + }); +</script> +{% endblock %} \ No newline at end of file