@aprovan/patchwork-web 0.1.0-dev.4d82df8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +327 -0
- package/.utcp_config.json +65 -0
- package/LICENSE +373 -0
- package/dist/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist/assets/abap-BdImnpbu.js +1 -0
- package/dist/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/assets/ada-bCR0ucgS.js +1 -0
- package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/assets/angular-html-CU67Zn6k.js +1 -0
- package/dist/assets/angular-ts-BwZT4LLn.js +1 -0
- package/dist/assets/apache-Pmp26Uib.js +1 -0
- package/dist/assets/apex-D8_7TLub.js +1 -0
- package/dist/assets/apl-dKokRX4l.js +1 -0
- package/dist/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/assets/ara-BRHolxvo.js +1 -0
- package/dist/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/assets/astro-CbQHKStN.js +1 -0
- package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/assets/awk-DMzUqQB5.js +1 -0
- package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/assets/bat-BkioyH1T.js +1 -0
- package/dist/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/assets/berry-uYugtg8r.js +1 -0
- package/dist/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/assets/bird2-DPOp833l.js +1 -0
- package/dist/assets/blade-D4QpJJKB.js +1 -0
- package/dist/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/assets/c-BIGW1oBm.js +1 -0
- package/dist/assets/c3-eo99z4R2.js +1 -0
- package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/assets/clarity-D53aC0YG.js +1 -0
- package/dist/assets/clojure-P80f7IUj.js +1 -0
- package/dist/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/assets/cobol-nwyudZeR.js +1 -0
- package/dist/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/assets/coq-DkFqJrB1.js +1 -0
- package/dist/assets/cpp-CofmeUqb.js +1 -0
- package/dist/assets/crystal-tKQVLTB8.js +1 -0
- package/dist/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/assets/css-DPfMkruS.js +1 -0
- package/dist/assets/csv-fuZLfV_i.js +1 -0
- package/dist/assets/cue-D82EKSYY.js +1 -0
- package/dist/assets/cypher-COkxafJQ.js +1 -0
- package/dist/assets/d-85-TOEBH.js +1 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/dart-CF10PKvl.js +1 -0
- package/dist/assets/dax-CEL-wOlO.js +1 -0
- package/dist/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/assets/diff-D97Zzqfu.js +1 -0
- package/dist/assets/docker-BcOcwvcX.js +1 -0
- package/dist/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/assets/edge-BkV0erSs.js +1 -0
- package/dist/assets/elixir-CDX3lj18.js +1 -0
- package/dist/assets/elm-DbKCFpqz.js +1 -0
- package/dist/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/dist/assets/erb-B12qg9BL.js +1 -0
- package/dist/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/assets/fennel-BYunw83y.js +1 -0
- package/dist/assets/fish-BvzEVeQv.js +1 -0
- package/dist/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/assets/genie-D0YGMca9.js +1 -0
- package/dist/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/assets/gleam-BspZqrRM.js +1 -0
- package/dist/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/dist/assets/glimmer-ts-U6CK756n.js +1 -0
- package/dist/assets/glsl-DplSGwfg.js +1 -0
- package/dist/assets/gn-n2N0HUVH.js +1 -0
- package/dist/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/assets/go-CxLEBnE3.js +1 -0
- package/dist/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/assets/hack-CaT9iCJl.js +1 -0
- package/dist/assets/haml-B8DHNrY2.js +1 -0
- package/dist/assets/handlebars-BL8al0AC.js +1 -0
- package/dist/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/assets/hjson-D5-asLiD.js +1 -0
- package/dist/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/assets/horizon-bright-Cn-bp-IR.js +1 -0
- package/dist/assets/houston-DnULxvSX.js +1 -0
- package/dist/assets/html-GMplVEZG.js +1 -0
- package/dist/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/dist/assets/http-jrhK8wxY.js +1 -0
- package/dist/assets/hurl-irOxFIW8.js +1 -0
- package/dist/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/assets/hy-DFXneXwc.js +1 -0
- package/dist/assets/imba-DGztddWO.js +1 -0
- package/dist/assets/index-B_lEiI-9.js +1544 -0
- package/dist/assets/index-CQT9NtoK.css +1 -0
- package/dist/assets/ini-BEwlwnbL.js +1 -0
- package/dist/assets/java-CylS5w8V.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/jinja-4LBKfQ-Z.js +1 -0
- package/dist/assets/jison-wvAkD_A8.js +1 -0
- package/dist/assets/json-Cp-IABpG.js +1 -0
- package/dist/assets/json5-C9tS-k6U.js +1 -0
- package/dist/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/assets/julia-CxzCAyBv.js +1 -0
- package/dist/assets/just-Cw27pwNe.js +1 -0
- package/dist/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/assets/kdl-DV7GczEv.js +1 -0
- package/dist/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/assets/kusto-DZf3V79B.js +1 -0
- package/dist/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/assets/less-B1dDrJ26.js +1 -0
- package/dist/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/assets/liquid-DYVedYrR.js +1 -0
- package/dist/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/assets/log-2UxHyX5q.js +1 -0
- package/dist/assets/logo-BtOb2qkB.js +1 -0
- package/dist/assets/lua-BaeVxFsk.js +1 -0
- package/dist/assets/luau-C-HG3fhB.js +1 -0
- package/dist/assets/make-CHLpvVh8.js +1 -0
- package/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/assets/marko-CnJfTvn9.js +1 -0
- package/dist/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/assets/matlab-D7o27uSR.js +1 -0
- package/dist/assets/mdc-BMNejdWA.js +1 -0
- package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/assets/min-light-CTRr51gU.js +1 -0
- package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/assets/move-IF9eRakj.js +1 -0
- package/dist/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/assets/nextflow-Zz6hmt5N.js +1 -0
- package/dist/assets/nextflow-groovy-BeH2EWoN.js +1 -0
- package/dist/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/assets/nim-CVrawwO9.js +1 -0
- package/dist/assets/nix-CwoSXNpI.js +1 -0
- package/dist/assets/nord-Ddv68eIx.js +1 -0
- package/dist/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/assets/odin-BBf5iR-q.js +1 -0
- package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/assets/perl-C0TMdlhV.js +1 -0
- package/dist/assets/php-Dhbhpdrm.js +1 -0
- package/dist/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/assets/po-BTJTHyun.js +1 -0
- package/dist/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/assets/polar-C0HS_06l.js +1 -0
- package/dist/assets/postcss-CXtECtnM.js +1 -0
- package/dist/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/assets/pug-CGlum2m_.js +1 -0
- package/dist/assets/puppet-BMWR74SV.js +1 -0
- package/dist/assets/purescript-CklMAg4u.js +1 -0
- package/dist/assets/python-B6aJPvgy.js +1 -0
- package/dist/assets/qml-3beO22l8.js +1 -0
- package/dist/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/assets/qss-IeuSbFQv.js +1 -0
- package/dist/assets/r-Dspwwk_N.js +1 -0
- package/dist/assets/racket-BqYA7rlc.js +1 -0
- package/dist/assets/raku-DXvB9xmW.js +1 -0
- package/dist/assets/razor-Uh8Bk_45.js +1 -0
- package/dist/assets/red-bN70gL4F.js +1 -0
- package/dist/assets/reg-C-SQnVFl.js +1 -0
- package/dist/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/assets/rel-C3B-1QV4.js +1 -0
- package/dist/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/assets/ron-D8l8udqQ.js +1 -0
- package/dist/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/assets/rst-BrH8l1NY.js +1 -0
- package/dist/assets/ruby-Dw2BHqvy.js +1 -0
- package/dist/assets/rust-B1yitclQ.js +1 -0
- package/dist/assets/sas-cz2c8ADy.js +1 -0
- package/dist/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/assets/scala-C151Ov-r.js +1 -0
- package/dist/assets/scheme-C98Dy4si.js +1 -0
- package/dist/assets/scss-OYdSNvt2.js +1 -0
- package/dist/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/assets/solidity-rGO070M0.js +1 -0
- package/dist/assets/soy-Brmx7dQM.js +1 -0
- package/dist/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/assets/sql-BLtJtn59.js +1 -0
- package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/assets/stata-BH5u7GGu.js +1 -0
- package/dist/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/assets/svelte-C_ipcX3V.js +1 -0
- package/dist/assets/swift-D82vCrfD.js +1 -0
- package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/assets/templ-P3uqSqPl.js +1 -0
- package/dist/assets/terraform-BETggiCN.js +1 -0
- package/dist/assets/tex-idrVyKtj.js +1 -0
- package/dist/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/assets/toml-vGWfd6FD.js +1 -0
- package/dist/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/dist/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/assets/tsx-COt5Ahok.js +1 -0
- package/dist/assets/turtle-BsS91CYL.js +1 -0
- package/dist/assets/twig-DNn4PbVi.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/typespec-BGHnOYBU.js +1 -0
- package/dist/assets/typst-DHCkPAjA.js +1 -0
- package/dist/assets/v-BcVCzyr7.js +1 -0
- package/dist/assets/vala-CsfeWuGM.js +1 -0
- package/dist/assets/vb-D17OF-Vu.js +1 -0
- package/dist/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/assets/vesper-DU1UobuO.js +1 -0
- package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/assets/viml-CJc9bBzg.js +1 -0
- package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/assets/vue-DN_0RTcg.js +1 -0
- package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/assets/vue-vine-CQOfvN7w.js +1 -0
- package/dist/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/assets/wit-5i3qLPDT.js +1 -0
- package/dist/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/assets/yaml-Buea-lGh.js +1 -0
- package/dist/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/assets/zig-VOosw3JB.js +1 -0
- package/dist/index.html +18 -0
- package/index.html +17 -0
- package/package.json +54 -0
- package/postcss.config.js +6 -0
- package/scripts/start-dev.ts +68 -0
- package/src/App.tsx +7 -0
- package/src/components/WorkspaceSwitcher.tsx +125 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +85 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +60 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/scroll-area.tsx +45 -0
- package/src/index.css +190 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/workspace-vfs.ts +190 -0
- package/src/main.tsx +10 -0
- package/src/pages/ChatPage.tsx +1098 -0
- package/tailwind.config.js +73 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +18 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
import { useChat } from "@ai-sdk/react";
|
|
2
|
+
import { Bobbin } from '@aprovan/bobbin';
|
|
3
|
+
import { createCompiler, type Compiler , type VirtualProject } from "@aprovan/patchwork-compiler";
|
|
4
|
+
import {
|
|
5
|
+
extractCodeBlocks,
|
|
6
|
+
CodePreview,
|
|
7
|
+
WidgetPreview,
|
|
8
|
+
MarkdownEditor,
|
|
9
|
+
ServicesInspector,
|
|
10
|
+
EditModal,
|
|
11
|
+
FileTree,
|
|
12
|
+
type ServiceInfo,
|
|
13
|
+
} from "@aprovan/patchwork-editor";
|
|
14
|
+
import { DefaultChatTransport } from "ai";
|
|
15
|
+
import {
|
|
16
|
+
Send,
|
|
17
|
+
Loader2,
|
|
18
|
+
Wrench,
|
|
19
|
+
AlertCircle,
|
|
20
|
+
Brain,
|
|
21
|
+
ChevronDown,
|
|
22
|
+
Minus,
|
|
23
|
+
RefreshCw,
|
|
24
|
+
X,
|
|
25
|
+
} from "lucide-react";
|
|
26
|
+
import {
|
|
27
|
+
useState,
|
|
28
|
+
useRef,
|
|
29
|
+
useEffect,
|
|
30
|
+
useMemo,
|
|
31
|
+
useCallback,
|
|
32
|
+
createContext,
|
|
33
|
+
useContext,
|
|
34
|
+
} from "react";
|
|
35
|
+
import Markdown from "react-markdown";
|
|
36
|
+
import remarkGfm from "remark-gfm";
|
|
37
|
+
import type { UIMessage } from "ai";
|
|
38
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
39
|
+
import { Badge } from "@/components/ui/badge";
|
|
40
|
+
import { Button } from "@/components/ui/button";
|
|
41
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
42
|
+
import {
|
|
43
|
+
Collapsible,
|
|
44
|
+
CollapsibleContent,
|
|
45
|
+
CollapsibleTrigger,
|
|
46
|
+
} from "@/components/ui/collapsible";
|
|
47
|
+
import {
|
|
48
|
+
Dialog,
|
|
49
|
+
DialogHeader,
|
|
50
|
+
DialogContent,
|
|
51
|
+
DialogClose,
|
|
52
|
+
} from "@/components/ui/dialog";
|
|
53
|
+
import { Input } from "@/components/ui/input";
|
|
54
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
55
|
+
import WorkspaceSwitcher from "@/components/WorkspaceSwitcher";
|
|
56
|
+
import {
|
|
57
|
+
listWorkspaceEntries,
|
|
58
|
+
listWorkspacePaths,
|
|
59
|
+
toWorkspaceTreeFiles,
|
|
60
|
+
loadWorkspaceDirectoryProject,
|
|
61
|
+
loadWorkspaceFileProject,
|
|
62
|
+
createSingleWorkspaceFileProject,
|
|
63
|
+
saveWorkspaceProject,
|
|
64
|
+
subscribeToWorkspaceChanges,
|
|
65
|
+
resetStore,
|
|
66
|
+
} from "@/lib/workspace-vfs";
|
|
67
|
+
|
|
68
|
+
const APROVAN_LOGO =
|
|
69
|
+
"https://raw.githubusercontent.com/AprovanLabs/aprovan.com/main/docs/assets/social-labs.png";
|
|
70
|
+
|
|
71
|
+
interface PatchworkContext {
|
|
72
|
+
compiler: Compiler | null;
|
|
73
|
+
namespaces: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PatchworkCtx = createContext<PatchworkContext>({
|
|
77
|
+
compiler: null,
|
|
78
|
+
namespaces: [],
|
|
79
|
+
});
|
|
80
|
+
const useCompiler = () => useContext(PatchworkCtx).compiler;
|
|
81
|
+
const useServices = () => useContext(PatchworkCtx).namespaces;
|
|
82
|
+
|
|
83
|
+
function createPreviewManifest(services?: string[]) {
|
|
84
|
+
return {
|
|
85
|
+
name: "preview",
|
|
86
|
+
version: "1.0.0",
|
|
87
|
+
platform: "browser" as const,
|
|
88
|
+
image: "@aprovan/patchwork-image-shadcn",
|
|
89
|
+
services,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const SharedEditSessionCtx = createContext<
|
|
94
|
+
| ((session: {
|
|
95
|
+
projectId: string;
|
|
96
|
+
entryFile: string;
|
|
97
|
+
filePath?: string;
|
|
98
|
+
initialCode: string;
|
|
99
|
+
initialProject: VirtualProject;
|
|
100
|
+
}) => void)
|
|
101
|
+
| null
|
|
102
|
+
>(null);
|
|
103
|
+
|
|
104
|
+
const useSharedEditSession = () => useContext(SharedEditSessionCtx);
|
|
105
|
+
|
|
106
|
+
function ReasoningPart({
|
|
107
|
+
text,
|
|
108
|
+
isStreaming,
|
|
109
|
+
}: {
|
|
110
|
+
text: string;
|
|
111
|
+
isStreaming?: boolean;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<Collapsible defaultOpen={isStreaming}>
|
|
115
|
+
<CollapsibleTrigger className="flex items-center gap-2 text-yellow-700 dark:text-yellow-400 hover:opacity-80 w-full">
|
|
116
|
+
<Brain className="h-4 w-4" />
|
|
117
|
+
<span className="text-xs font-medium">Thinking</span>
|
|
118
|
+
{isStreaming && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
119
|
+
<ChevronDown className="h-3 w-3 ml-auto transition-transform [[data-state=open]>&]:rotate-180" />
|
|
120
|
+
</CollapsibleTrigger>
|
|
121
|
+
<CollapsibleContent>
|
|
122
|
+
<div className="mt-2 p-3 rounded border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-950/50">
|
|
123
|
+
<p className="text-sm text-muted-foreground italic whitespace-pre-wrap">
|
|
124
|
+
{text}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</CollapsibleContent>
|
|
128
|
+
</Collapsible>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ToolPart({
|
|
133
|
+
toolName,
|
|
134
|
+
state,
|
|
135
|
+
input,
|
|
136
|
+
output,
|
|
137
|
+
errorText,
|
|
138
|
+
}: {
|
|
139
|
+
toolName: string;
|
|
140
|
+
state: string;
|
|
141
|
+
input: unknown;
|
|
142
|
+
output?: unknown;
|
|
143
|
+
errorText?: string;
|
|
144
|
+
}) {
|
|
145
|
+
const isRunning = state === "input-streaming" || state === "input-available";
|
|
146
|
+
const hasError = state === "output-error";
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Collapsible className="my-1 w-full">
|
|
150
|
+
<CollapsibleTrigger className="inline-flex items-center gap-2 px-3 py-1 rounded-full border bg-muted/50 hover:bg-muted text-xs transition-colors">
|
|
151
|
+
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
152
|
+
<span className="font-mono">{toolName}</span>
|
|
153
|
+
<span className="w-3 h-3 flex items-center justify-center">
|
|
154
|
+
{isRunning && (
|
|
155
|
+
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
|
|
156
|
+
)}
|
|
157
|
+
{hasError && <AlertCircle className="h-3 w-3 text-destructive" />}
|
|
158
|
+
</span>
|
|
159
|
+
<ChevronDown className="h-3 w-3 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-180" />
|
|
160
|
+
</CollapsibleTrigger>
|
|
161
|
+
|
|
162
|
+
<CollapsibleContent className="mt-2 p-3 rounded-lg border bg-white space-y-2">
|
|
163
|
+
{input !== undefined && (
|
|
164
|
+
<div>
|
|
165
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
166
|
+
Input
|
|
167
|
+
</span>
|
|
168
|
+
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
169
|
+
<pre className="whitespace-pre-wrap break-words m-0">
|
|
170
|
+
{typeof input === "string"
|
|
171
|
+
? input
|
|
172
|
+
: JSON.stringify(input, null, 2)}
|
|
173
|
+
</pre>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{output !== undefined && (
|
|
179
|
+
<div>
|
|
180
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
181
|
+
Output
|
|
182
|
+
</span>
|
|
183
|
+
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
184
|
+
<pre className="whitespace-pre-wrap break-words m-0">
|
|
185
|
+
{typeof output === "string"
|
|
186
|
+
? output
|
|
187
|
+
: JSON.stringify(output, null, 2)}
|
|
188
|
+
</pre>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{errorText && (
|
|
194
|
+
<div className="text-sm text-destructive flex items-center gap-2">
|
|
195
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
196
|
+
<span className="break-words">{errorText}</span>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</CollapsibleContent>
|
|
200
|
+
</Collapsible>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function MessageBubble({ message }: { message: UIMessage }) {
|
|
205
|
+
const isUser = message.role === "user";
|
|
206
|
+
const isStreaming = message.parts?.some(
|
|
207
|
+
(p) =>
|
|
208
|
+
"state" in p &&
|
|
209
|
+
(p.state === "input-streaming" || p.state === "input-available"),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className={`flex gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
|
214
|
+
{!isUser && (
|
|
215
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
216
|
+
<img src={APROVAN_LOGO} alt="Assistant" className="rounded-full" />
|
|
217
|
+
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
218
|
+
A
|
|
219
|
+
</AvatarFallback>
|
|
220
|
+
</Avatar>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<div
|
|
224
|
+
className={`flex flex-col gap-1 max-w-[80%] min-w-0 ${
|
|
225
|
+
isUser ? "items-end" : "items-start"
|
|
226
|
+
}`}
|
|
227
|
+
>
|
|
228
|
+
<div className="flex items-center gap-2 h-5">
|
|
229
|
+
<span className="text-xs text-muted-foreground capitalize">
|
|
230
|
+
{message.role}
|
|
231
|
+
</span>
|
|
232
|
+
{isStreaming && (
|
|
233
|
+
<Badge variant="outline" className="text-xs">
|
|
234
|
+
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
|
235
|
+
streaming
|
|
236
|
+
</Badge>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div
|
|
241
|
+
className={`rounded-lg px-4 py-2 overflow-hidden w-full ${
|
|
242
|
+
isUser ? "bg-primary text-primary-foreground" : "bg-muted"
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
{message.parts?.map((part, i) => {
|
|
246
|
+
if (part.type === "text") {
|
|
247
|
+
return (
|
|
248
|
+
<TextPartWithSession key={i} text={part.text} isUser={isUser} />
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (part.type === "reasoning") {
|
|
253
|
+
return (
|
|
254
|
+
<ReasoningPart
|
|
255
|
+
key={i}
|
|
256
|
+
text={part.text}
|
|
257
|
+
isStreaming={part.state === "streaming"}
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
|
|
263
|
+
const toolPart = part as {
|
|
264
|
+
type: string;
|
|
265
|
+
toolName?: string;
|
|
266
|
+
toolCallId: string;
|
|
267
|
+
state: string;
|
|
268
|
+
input?: unknown;
|
|
269
|
+
output?: unknown;
|
|
270
|
+
errorText?: string;
|
|
271
|
+
};
|
|
272
|
+
const toolName =
|
|
273
|
+
toolPart.toolName ?? part.type.replace("tool-", "");
|
|
274
|
+
return (
|
|
275
|
+
<ToolPart
|
|
276
|
+
key={i}
|
|
277
|
+
toolName={toolName}
|
|
278
|
+
state={toolPart.state}
|
|
279
|
+
input={toolPart.input}
|
|
280
|
+
output={toolPart.output}
|
|
281
|
+
errorText={toolPart.errorText}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return null;
|
|
287
|
+
})}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{isUser && (
|
|
292
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
293
|
+
<AvatarFallback className="bg-secondary">U</AvatarFallback>
|
|
294
|
+
</Avatar>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function TextPartWithSession({
|
|
301
|
+
text,
|
|
302
|
+
isUser,
|
|
303
|
+
}: {
|
|
304
|
+
text: string;
|
|
305
|
+
isUser: boolean;
|
|
306
|
+
}) {
|
|
307
|
+
const open = useSharedEditSession();
|
|
308
|
+
const compiler = useCompiler();
|
|
309
|
+
const services = useServices();
|
|
310
|
+
|
|
311
|
+
if (isUser) {
|
|
312
|
+
return (
|
|
313
|
+
<div className="prose prose-sm prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
|
|
314
|
+
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parts = extractCodeBlocks(text);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className="prose prose-sm dark:prose-invert prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 prose-pre:my-2 prose-code:before:content-none prose-code:after:content-none">
|
|
323
|
+
{parts.map((part, index) => {
|
|
324
|
+
if (part.type === "code") {
|
|
325
|
+
return (
|
|
326
|
+
<CodePreview
|
|
327
|
+
key={index}
|
|
328
|
+
code={part.content}
|
|
329
|
+
compiler={compiler}
|
|
330
|
+
services={services}
|
|
331
|
+
filePath={part.attributes?.path}
|
|
332
|
+
entrypoint="main.tsx"
|
|
333
|
+
onOpenEditSession={open ?? undefined}
|
|
334
|
+
/>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return (
|
|
338
|
+
<Markdown key={index} remarkPlugins={[remarkGfm]}>
|
|
339
|
+
{part.content}
|
|
340
|
+
</Markdown>
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const PROXY_URL = "/api/proxy";
|
|
348
|
+
const IMAGE_SPEC = "@aprovan/patchwork-image-shadcn";
|
|
349
|
+
// Local proxy for loading image packages, esm.sh for widget imports
|
|
350
|
+
const IMAGE_CDN_URL = import.meta.env.DEV
|
|
351
|
+
? "/_local-packages"
|
|
352
|
+
: "https://esm.sh";
|
|
353
|
+
const WIDGET_CDN_URL = "https://esm.sh"; // Widget imports need esm.sh bundles like @packagedcn
|
|
354
|
+
|
|
355
|
+
function toProjectRelativePath(projectId: string, path: string): string {
|
|
356
|
+
const normalizedProjectId = projectId.replace(/^\/+|\/+$/g, "");
|
|
357
|
+
const normalizedPath = path.replace(/^\/+|\/+$/g, "");
|
|
358
|
+
if (!normalizedProjectId) return normalizedPath;
|
|
359
|
+
const prefix = `${normalizedProjectId}/`;
|
|
360
|
+
if (normalizedPath.startsWith(prefix)) {
|
|
361
|
+
return normalizedPath.slice(prefix.length);
|
|
362
|
+
}
|
|
363
|
+
return normalizedPath;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const TABS_KEY_PREFIX = 'patchwork:open-tabs';
|
|
367
|
+
const ACTIVE_WORKSPACE_KEY = 'patchwork:active-workspace';
|
|
368
|
+
|
|
369
|
+
function getTabsStorageKey(workspaceId: string | null): string {
|
|
370
|
+
return workspaceId ? `${TABS_KEY_PREFIX}:${workspaceId}` : TABS_KEY_PREFIX;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function loadPersistedTabState(workspaceId: string | null): { paths: string[]; activePath: string | null } {
|
|
374
|
+
try {
|
|
375
|
+
const raw = localStorage.getItem(getTabsStorageKey(workspaceId));
|
|
376
|
+
if (!raw) return { paths: [], activePath: null };
|
|
377
|
+
const parsed = JSON.parse(raw);
|
|
378
|
+
return {
|
|
379
|
+
paths: Array.isArray(parsed.paths) ? parsed.paths : [],
|
|
380
|
+
activePath: typeof parsed.activePath === 'string' ? parsed.activePath : null,
|
|
381
|
+
};
|
|
382
|
+
} catch {
|
|
383
|
+
return { paths: [], activePath: null };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function persistTabState(paths: string[], activePath: string | null, workspaceId: string | null) {
|
|
388
|
+
localStorage.setItem(getTabsStorageKey(workspaceId), JSON.stringify({ paths, activePath }));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default function ChatPage() {
|
|
392
|
+
const [input, setInput] = useState(
|
|
393
|
+
"What's the weather in Houston, Texas like?",
|
|
394
|
+
);
|
|
395
|
+
const [compiler, setCompiler] = useState<Compiler | null>(null);
|
|
396
|
+
const [namespaces, setNamespaces] = useState<string[]>([]);
|
|
397
|
+
const [services, setServices] = useState<ServiceInfo[]>([]);
|
|
398
|
+
const [workspaceFiles, setWorkspaceFiles] = useState<string[]>([]);
|
|
399
|
+
const [workspaceActivePath, setWorkspaceActivePath] = useState("");
|
|
400
|
+
const [workspaceFilter, setWorkspaceFilter] = useState("");
|
|
401
|
+
const [workspaceTreeVersion, setWorkspaceTreeVersion] = useState(0);
|
|
402
|
+
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
|
403
|
+
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
|
|
404
|
+
const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(
|
|
405
|
+
() => localStorage.getItem(ACTIVE_WORKSPACE_KEY),
|
|
406
|
+
);
|
|
407
|
+
const [chatContainer, setChatContainer] = useState<HTMLDivElement | null>(
|
|
408
|
+
null,
|
|
409
|
+
);
|
|
410
|
+
const [editSession, setEditSession] = useState<{
|
|
411
|
+
project: VirtualProject;
|
|
412
|
+
initialTreePath?: string;
|
|
413
|
+
initialActiveFile?: string;
|
|
414
|
+
} | null>(null);
|
|
415
|
+
const [openTabs, setOpenTabs] = useState<
|
|
416
|
+
Map<string, { code: string; loading: boolean; error: string | null }>
|
|
417
|
+
>(() => {
|
|
418
|
+
const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
|
|
419
|
+
const { paths } = loadPersistedTabState(wsId);
|
|
420
|
+
return new Map(paths.map((p) => [p, { code: '', loading: true, error: null }]));
|
|
421
|
+
});
|
|
422
|
+
const [activeTabPath, setActiveTabPath] = useState<string | null>(() => {
|
|
423
|
+
const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
|
|
424
|
+
const { paths, activePath } = loadPersistedTabState(wsId);
|
|
425
|
+
if (activePath && paths.includes(activePath)) return activePath;
|
|
426
|
+
return paths[0] ?? null;
|
|
427
|
+
});
|
|
428
|
+
const [previewCollapsed, setPreviewCollapsed] = useState(false);
|
|
429
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
430
|
+
const tabRequestRefs = useRef<Map<string, number>>(new Map());
|
|
431
|
+
|
|
432
|
+
const [pinnedPaths, setPinnedPaths] = useState<Map<string, boolean>>(() => {
|
|
433
|
+
try {
|
|
434
|
+
const stored = localStorage.getItem('patchwork:pinned-paths');
|
|
435
|
+
if (!stored) return new Map();
|
|
436
|
+
const parsed = JSON.parse(stored) as Array<[string, boolean]> | string[];
|
|
437
|
+
if (parsed.length > 0 && Array.isArray(parsed[0])) {
|
|
438
|
+
return new Map(parsed as Array<[string, boolean]>);
|
|
439
|
+
}
|
|
440
|
+
return new Map((parsed as string[]).map((p) => [p, false]));
|
|
441
|
+
} catch {
|
|
442
|
+
return new Map();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const togglePin = useCallback((path: string, isDir: boolean) => {
|
|
447
|
+
setPinnedPaths((prev) => {
|
|
448
|
+
const next = new Map(prev);
|
|
449
|
+
if (next.has(path)) next.delete(path);
|
|
450
|
+
else next.set(path, isDir);
|
|
451
|
+
localStorage.setItem('patchwork:pinned-paths', JSON.stringify(Array.from(next)));
|
|
452
|
+
return next;
|
|
453
|
+
});
|
|
454
|
+
}, []);
|
|
455
|
+
|
|
456
|
+
const refreshWorkspace = useCallback(async () => {
|
|
457
|
+
setWorkspaceLoading(true);
|
|
458
|
+
setWorkspaceError(null);
|
|
459
|
+
try {
|
|
460
|
+
if (workspaceFilter.trim()) {
|
|
461
|
+
const paths = await listWorkspacePaths();
|
|
462
|
+
setWorkspaceFiles(paths);
|
|
463
|
+
}
|
|
464
|
+
setWorkspaceTreeVersion((prev) => prev + 1);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
setWorkspaceError(
|
|
467
|
+
err instanceof Error ? err.message : "Failed to load workspace",
|
|
468
|
+
);
|
|
469
|
+
} finally {
|
|
470
|
+
setWorkspaceLoading(false);
|
|
471
|
+
}
|
|
472
|
+
}, [workspaceFilter]);
|
|
473
|
+
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
return subscribeToWorkspaceChanges(() => {
|
|
476
|
+
setWorkspaceTreeVersion((prev) => prev + 1);
|
|
477
|
+
listWorkspacePaths()
|
|
478
|
+
.then((allPaths) => {
|
|
479
|
+
if (workspaceFilter.trim()) setWorkspaceFiles(allPaths);
|
|
480
|
+
const existing = new Set(allPaths);
|
|
481
|
+
setOpenTabs((prev) => {
|
|
482
|
+
let changed = false;
|
|
483
|
+
const next = new Map(prev);
|
|
484
|
+
for (const path of next.keys()) {
|
|
485
|
+
if (!existing.has(path)) { next.delete(path); changed = true; }
|
|
486
|
+
}
|
|
487
|
+
return changed ? next : prev;
|
|
488
|
+
});
|
|
489
|
+
})
|
|
490
|
+
.catch(() => {});
|
|
491
|
+
});
|
|
492
|
+
}, [workspaceFilter]);
|
|
493
|
+
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
if (!workspaceFilter.trim()) return;
|
|
496
|
+
|
|
497
|
+
setWorkspaceLoading(true);
|
|
498
|
+
setWorkspaceError(null);
|
|
499
|
+
|
|
500
|
+
listWorkspacePaths()
|
|
501
|
+
.then(setWorkspaceFiles)
|
|
502
|
+
.catch((err) => {
|
|
503
|
+
setWorkspaceError(
|
|
504
|
+
err instanceof Error ? err.message : "Failed to load workspace",
|
|
505
|
+
);
|
|
506
|
+
})
|
|
507
|
+
.finally(() => setWorkspaceLoading(false));
|
|
508
|
+
}, [workspaceFilter]);
|
|
509
|
+
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
// Fetch available services
|
|
512
|
+
fetch("/api/services")
|
|
513
|
+
.then((res) => res.json())
|
|
514
|
+
.then((data) => {
|
|
515
|
+
setNamespaces(data.namespaces ?? []);
|
|
516
|
+
// In dev mode, also store full service details for inspection
|
|
517
|
+
if (import.meta.env.DEV && data.services) {
|
|
518
|
+
setServices(data.services);
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
.catch(() => {
|
|
522
|
+
setNamespaces([]);
|
|
523
|
+
setServices([]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Initialize compiler
|
|
527
|
+
createCompiler({
|
|
528
|
+
image: IMAGE_SPEC,
|
|
529
|
+
proxyUrl: PROXY_URL,
|
|
530
|
+
cdnBaseUrl: IMAGE_CDN_URL,
|
|
531
|
+
widgetCdnBaseUrl: WIDGET_CDN_URL,
|
|
532
|
+
})
|
|
533
|
+
.then(setCompiler)
|
|
534
|
+
.catch(console.error);
|
|
535
|
+
|
|
536
|
+
void refreshWorkspace();
|
|
537
|
+
}, []);
|
|
538
|
+
|
|
539
|
+
// Load content for tabs restored from localStorage
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
openTabs.forEach((tab, path) => {
|
|
542
|
+
if (!tab.loading) return;
|
|
543
|
+
const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
|
|
544
|
+
tabRequestRefs.current.set(path, requestId);
|
|
545
|
+
loadWorkspaceFileProject(path)
|
|
546
|
+
.then((project) => {
|
|
547
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
548
|
+
if (!project) {
|
|
549
|
+
setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const file = project.files.get(project.entry);
|
|
553
|
+
setOpenTabs((prev) => {
|
|
554
|
+
const next = new Map(prev);
|
|
555
|
+
next.set(path, { code: file?.content ?? '', loading: false, error: null });
|
|
556
|
+
return next;
|
|
557
|
+
});
|
|
558
|
+
})
|
|
559
|
+
.catch(() => {
|
|
560
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
561
|
+
setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}, []);
|
|
565
|
+
|
|
566
|
+
// Persist open tabs to localStorage (scoped by active workspace)
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
persistTabState([...openTabs.keys()], activeTabPath, activeWorkspaceId);
|
|
569
|
+
}, [openTabs, activeTabPath, activeWorkspaceId]);
|
|
570
|
+
|
|
571
|
+
// Fix activeTabPath when its tab is removed
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (activeTabPath !== null && !openTabs.has(activeTabPath)) {
|
|
574
|
+
setActiveTabPath([...openTabs.keys()][0] ?? null);
|
|
575
|
+
}
|
|
576
|
+
}, [openTabs, activeTabPath]);
|
|
577
|
+
|
|
578
|
+
const openSharedEditSession = useCallback(
|
|
579
|
+
async (session: {
|
|
580
|
+
projectId: string;
|
|
581
|
+
entryFile: string;
|
|
582
|
+
filePath?: string;
|
|
583
|
+
initialCode: string;
|
|
584
|
+
initialProject: VirtualProject;
|
|
585
|
+
}) => {
|
|
586
|
+
const { projectId, filePath, entryFile, initialCode, initialProject } =
|
|
587
|
+
session;
|
|
588
|
+
const directoryProject = await loadWorkspaceDirectoryProject(projectId);
|
|
589
|
+
const filePathKey = filePath ?? `${projectId}/${entryFile}`;
|
|
590
|
+
|
|
591
|
+
if (directoryProject) {
|
|
592
|
+
const relativePath = toProjectRelativePath(projectId, filePathKey);
|
|
593
|
+
setWorkspaceActivePath(filePathKey);
|
|
594
|
+
setEditSession({
|
|
595
|
+
project: directoryProject,
|
|
596
|
+
initialTreePath: relativePath,
|
|
597
|
+
initialActiveFile: relativePath,
|
|
598
|
+
});
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const fallbackFilePath = filePathKey;
|
|
603
|
+
const fallbackProject = filePath
|
|
604
|
+
? createSingleWorkspaceFileProject(filePath, initialCode)
|
|
605
|
+
: initialProject;
|
|
606
|
+
setWorkspaceActivePath(fallbackFilePath);
|
|
607
|
+
setEditSession({
|
|
608
|
+
project: fallbackProject,
|
|
609
|
+
initialTreePath: fallbackProject.entry,
|
|
610
|
+
initialActiveFile: fallbackProject.entry,
|
|
611
|
+
});
|
|
612
|
+
},
|
|
613
|
+
[],
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const openWorkspaceSession = useCallback(
|
|
617
|
+
async (path: string, isDir: boolean) => {
|
|
618
|
+
const project = isDir
|
|
619
|
+
? await loadWorkspaceDirectoryProject(path)
|
|
620
|
+
: await loadWorkspaceFileProject(path);
|
|
621
|
+
if (!project) return;
|
|
622
|
+
|
|
623
|
+
setWorkspaceActivePath(path);
|
|
624
|
+
setEditSession({
|
|
625
|
+
project,
|
|
626
|
+
initialTreePath: project.entry,
|
|
627
|
+
initialActiveFile: project.entry,
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
[],
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const openWorkspacePreview = useCallback((path: string) => {
|
|
634
|
+
setWorkspaceActivePath(path);
|
|
635
|
+
setActiveTabPath(path);
|
|
636
|
+
setPreviewCollapsed(false);
|
|
637
|
+
|
|
638
|
+
// If tab already open, just activate it
|
|
639
|
+
setOpenTabs((prev) => {
|
|
640
|
+
if (prev.has(path)) return prev;
|
|
641
|
+
const next = new Map(prev);
|
|
642
|
+
next.set(path, { code: "", loading: true, error: null });
|
|
643
|
+
return next;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
|
|
647
|
+
tabRequestRefs.current.set(path, requestId);
|
|
648
|
+
|
|
649
|
+
void loadWorkspaceFileProject(path)
|
|
650
|
+
.then((project) => {
|
|
651
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
652
|
+
if (!project) {
|
|
653
|
+
setOpenTabs((prev) => {
|
|
654
|
+
const next = new Map(prev);
|
|
655
|
+
next.set(path, { code: "", loading: false, error: "Failed to load file preview" });
|
|
656
|
+
return next;
|
|
657
|
+
});
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const file = project.files.get(project.entry);
|
|
661
|
+
setOpenTabs((prev) => {
|
|
662
|
+
const next = new Map(prev);
|
|
663
|
+
next.set(path, { code: file?.content ?? "", loading: false, error: null });
|
|
664
|
+
return next;
|
|
665
|
+
});
|
|
666
|
+
})
|
|
667
|
+
.catch((err) => {
|
|
668
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
669
|
+
setOpenTabs((prev) => {
|
|
670
|
+
const next = new Map(prev);
|
|
671
|
+
next.set(path, {
|
|
672
|
+
code: "",
|
|
673
|
+
loading: false,
|
|
674
|
+
error: err instanceof Error ? err.message : "Failed to load file preview",
|
|
675
|
+
});
|
|
676
|
+
return next;
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}, []);
|
|
680
|
+
|
|
681
|
+
const closeTab = useCallback((path: string) => {
|
|
682
|
+
setOpenTabs((prev) => {
|
|
683
|
+
const next = new Map(prev);
|
|
684
|
+
next.delete(path);
|
|
685
|
+
return next;
|
|
686
|
+
});
|
|
687
|
+
setActiveTabPath((prev) => {
|
|
688
|
+
if (prev !== path) return prev;
|
|
689
|
+
// Activate an adjacent tab
|
|
690
|
+
const paths = [...openTabs.keys()];
|
|
691
|
+
const idx = paths.indexOf(path);
|
|
692
|
+
if (paths.length <= 1) return null;
|
|
693
|
+
return paths[idx > 0 ? idx - 1 : idx + 1] ?? null;
|
|
694
|
+
});
|
|
695
|
+
}, [openTabs]);
|
|
696
|
+
|
|
697
|
+
const closeAllTabs = useCallback(() => {
|
|
698
|
+
setOpenTabs(new Map());
|
|
699
|
+
setActiveTabPath(null);
|
|
700
|
+
}, []);
|
|
701
|
+
|
|
702
|
+
const handleWorkspaceSwitch = useCallback(
|
|
703
|
+
(newWorkspaceId: string) => {
|
|
704
|
+
localStorage.setItem(ACTIVE_WORKSPACE_KEY, newWorkspaceId);
|
|
705
|
+
setActiveWorkspaceId(newWorkspaceId);
|
|
706
|
+
setOpenTabs(new Map());
|
|
707
|
+
setActiveTabPath(null);
|
|
708
|
+
setPinnedPaths(new Map());
|
|
709
|
+
setEditSession(null);
|
|
710
|
+
resetStore();
|
|
711
|
+
void refreshWorkspace();
|
|
712
|
+
},
|
|
713
|
+
[refreshWorkspace],
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const handleWorkspaceLoad = useCallback(
|
|
717
|
+
(serverActiveId: string | null) => {
|
|
718
|
+
if (!serverActiveId) return;
|
|
719
|
+
const storedId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
|
|
720
|
+
if (serverActiveId === storedId) return;
|
|
721
|
+
// Server and localStorage disagree — trust the server
|
|
722
|
+
localStorage.setItem(ACTIVE_WORKSPACE_KEY, serverActiveId);
|
|
723
|
+
setActiveWorkspaceId(serverActiveId);
|
|
724
|
+
setOpenTabs(new Map());
|
|
725
|
+
setActiveTabPath(null);
|
|
726
|
+
setPinnedPaths(new Map());
|
|
727
|
+
resetStore();
|
|
728
|
+
void refreshWorkspace();
|
|
729
|
+
},
|
|
730
|
+
[refreshWorkspace],
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const filteredWorkspaceFiles = useMemo(() => {
|
|
734
|
+
const query = workspaceFilter.trim().toLowerCase();
|
|
735
|
+
if (!query) return workspaceFiles;
|
|
736
|
+
return workspaceFiles.filter((path) => path.toLowerCase().includes(query));
|
|
737
|
+
}, [workspaceFiles, workspaceFilter]);
|
|
738
|
+
|
|
739
|
+
const patchworkCtx = useMemo(
|
|
740
|
+
() => ({ compiler, namespaces }),
|
|
741
|
+
[compiler, namespaces],
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const transport = useMemo(
|
|
745
|
+
() =>
|
|
746
|
+
new DefaultChatTransport({
|
|
747
|
+
body: () => ({
|
|
748
|
+
prompt: {
|
|
749
|
+
id: "chat-patchwork-widget",
|
|
750
|
+
vars: { compilers: [IMAGE_SPEC] },
|
|
751
|
+
},
|
|
752
|
+
}),
|
|
753
|
+
}),
|
|
754
|
+
[],
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const { messages, sendMessage, status, error } = useChat({ transport });
|
|
758
|
+
|
|
759
|
+
const isLoading = status === "submitted" || status === "streaming";
|
|
760
|
+
|
|
761
|
+
const handleSubmit = useCallback(
|
|
762
|
+
(e?: React.FormEvent) => {
|
|
763
|
+
e?.preventDefault();
|
|
764
|
+
if (!input.trim()) return;
|
|
765
|
+
sendMessage({ text: input });
|
|
766
|
+
setInput("");
|
|
767
|
+
},
|
|
768
|
+
[input, sendMessage],
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
useEffect(() => {
|
|
772
|
+
scrollRef.current?.scrollTo({
|
|
773
|
+
top: scrollRef.current.scrollHeight,
|
|
774
|
+
behavior: "smooth",
|
|
775
|
+
});
|
|
776
|
+
}, [messages]);
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<PatchworkCtx.Provider value={patchworkCtx}>
|
|
780
|
+
<SharedEditSessionCtx.Provider value={openSharedEditSession}>
|
|
781
|
+
<div
|
|
782
|
+
className="flex flex-col h-screen max-w-6xl mx-auto p-4"
|
|
783
|
+
ref={setChatContainer}
|
|
784
|
+
>
|
|
785
|
+
<Card className="flex-1 flex flex-col min-h-0 overflow-hidden border">
|
|
786
|
+
<CardHeader className="border-b py-3">
|
|
787
|
+
<CardTitle className="flex items-center gap-3">
|
|
788
|
+
<img
|
|
789
|
+
src={APROVAN_LOGO}
|
|
790
|
+
alt="Aprovan"
|
|
791
|
+
className="h-8 w-8 rounded-full"
|
|
792
|
+
/>
|
|
793
|
+
<span className="text-lg">patchwork</span>
|
|
794
|
+
<WorkspaceSwitcher
|
|
795
|
+
onLoad={handleWorkspaceLoad}
|
|
796
|
+
onSwitch={handleWorkspaceSwitch}
|
|
797
|
+
/>
|
|
798
|
+
<ServicesInspector
|
|
799
|
+
namespaces={namespaces}
|
|
800
|
+
services={services}
|
|
801
|
+
DialogComponent={({ open, onOpenChange, children }) => (
|
|
802
|
+
<Dialog
|
|
803
|
+
open={open ?? false}
|
|
804
|
+
onOpenChange={onOpenChange ?? (() => undefined)}
|
|
805
|
+
>
|
|
806
|
+
{children}
|
|
807
|
+
</Dialog>
|
|
808
|
+
)}
|
|
809
|
+
DialogHeaderComponent={DialogHeader}
|
|
810
|
+
DialogContentComponent={DialogContent}
|
|
811
|
+
DialogCloseComponent={({ onClose }) => (
|
|
812
|
+
<DialogClose onClose={onClose ?? (() => undefined)} />
|
|
813
|
+
)}
|
|
814
|
+
/>
|
|
815
|
+
</CardTitle>
|
|
816
|
+
</CardHeader>
|
|
817
|
+
|
|
818
|
+
<CardContent className="flex-1 p-0 min-h-0 flex">
|
|
819
|
+
<div className="w-64 border-r bg-muted/20 min-h-0 flex flex-col min-w-sm">
|
|
820
|
+
<div className="px-3 py-2 border-b flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
821
|
+
<span>Workspace</span>
|
|
822
|
+
<button
|
|
823
|
+
onClick={() => void refreshWorkspace()}
|
|
824
|
+
className="ml-auto p-1 rounded hover:bg-muted"
|
|
825
|
+
title="Refresh workspace"
|
|
826
|
+
>
|
|
827
|
+
<RefreshCw
|
|
828
|
+
className={`h-3 w-3 ${
|
|
829
|
+
workspaceLoading ? "animate-spin" : ""
|
|
830
|
+
}`}
|
|
831
|
+
/>
|
|
832
|
+
</button>
|
|
833
|
+
</div>
|
|
834
|
+
<div className="p-2 border-b">
|
|
835
|
+
<Input
|
|
836
|
+
value={workspaceFilter}
|
|
837
|
+
onChange={(e) => setWorkspaceFilter(e.target.value)}
|
|
838
|
+
placeholder="Filter files..."
|
|
839
|
+
className="h-8"
|
|
840
|
+
/>
|
|
841
|
+
</div>
|
|
842
|
+
{workspaceError ? (
|
|
843
|
+
<div className="p-3 text-xs text-destructive">
|
|
844
|
+
{workspaceError}
|
|
845
|
+
</div>
|
|
846
|
+
) : workspaceFilter.trim() ? (
|
|
847
|
+
<FileTree
|
|
848
|
+
files={toWorkspaceTreeFiles(filteredWorkspaceFiles)}
|
|
849
|
+
activePath={workspaceActivePath}
|
|
850
|
+
onSelectFile={openWorkspacePreview}
|
|
851
|
+
onSelectDirectory={setWorkspaceActivePath}
|
|
852
|
+
onOpenInEditor={openWorkspaceSession}
|
|
853
|
+
openInEditorMode="all"
|
|
854
|
+
openInEditorTitle="Edit"
|
|
855
|
+
pinnedPaths={pinnedPaths}
|
|
856
|
+
onTogglePin={togglePin}
|
|
857
|
+
title="Files"
|
|
858
|
+
/>
|
|
859
|
+
) : (
|
|
860
|
+
<FileTree
|
|
861
|
+
files={[]}
|
|
862
|
+
activePath={workspaceActivePath}
|
|
863
|
+
onSelectFile={openWorkspacePreview}
|
|
864
|
+
onSelectDirectory={setWorkspaceActivePath}
|
|
865
|
+
onOpenInEditor={openWorkspaceSession}
|
|
866
|
+
openInEditorMode="all"
|
|
867
|
+
openInEditorTitle="Edit"
|
|
868
|
+
directoryLoader={listWorkspaceEntries}
|
|
869
|
+
pageSize={10}
|
|
870
|
+
reloadToken={workspaceTreeVersion}
|
|
871
|
+
pinnedPaths={pinnedPaths}
|
|
872
|
+
onTogglePin={togglePin}
|
|
873
|
+
title="Files"
|
|
874
|
+
/>
|
|
875
|
+
)}
|
|
876
|
+
</div>
|
|
877
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
878
|
+
{openTabs.size > 0 && (
|
|
879
|
+
<div className="border-b bg-muted/10">
|
|
880
|
+
{/* Tab bar */}
|
|
881
|
+
<div className="flex items-center border-b bg-muted/30">
|
|
882
|
+
<div className="flex-1 flex items-center overflow-x-auto min-w-0">
|
|
883
|
+
{[...openTabs.entries()].map(([path]) => {
|
|
884
|
+
const fileName = path.split("/").pop() ?? path;
|
|
885
|
+
const isActive = path === activeTabPath;
|
|
886
|
+
return (
|
|
887
|
+
<button
|
|
888
|
+
key={path}
|
|
889
|
+
onClick={() => {
|
|
890
|
+
setActiveTabPath(path);
|
|
891
|
+
setWorkspaceActivePath(path);
|
|
892
|
+
setPreviewCollapsed(false);
|
|
893
|
+
}}
|
|
894
|
+
className={`group relative flex items-center gap-1.5 px-3 py-1.5 text-xs border-r shrink-0 max-w-[180px] ${
|
|
895
|
+
isActive
|
|
896
|
+
? "bg-background text-foreground border-b-2 border-b-primary"
|
|
897
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
898
|
+
}`}
|
|
899
|
+
title={path}
|
|
900
|
+
>
|
|
901
|
+
<span className="truncate">{fileName}</span>
|
|
902
|
+
<span
|
|
903
|
+
role="button"
|
|
904
|
+
onClick={(e) => {
|
|
905
|
+
e.stopPropagation();
|
|
906
|
+
closeTab(path);
|
|
907
|
+
}}
|
|
908
|
+
className="shrink-0 p-0.5 rounded hover:bg-muted-foreground/20 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
909
|
+
title="Close tab"
|
|
910
|
+
>
|
|
911
|
+
<X className="h-3 w-3" />
|
|
912
|
+
</span>
|
|
913
|
+
</button>
|
|
914
|
+
);
|
|
915
|
+
})}
|
|
916
|
+
</div>
|
|
917
|
+
<div className="flex items-center gap-0.5 px-1 shrink-0">
|
|
918
|
+
<button
|
|
919
|
+
onClick={() => setPreviewCollapsed((p) => !p)}
|
|
920
|
+
className="p-1 rounded hover:bg-muted"
|
|
921
|
+
title={previewCollapsed ? "Expand preview" : "Collapse preview"}
|
|
922
|
+
>
|
|
923
|
+
{previewCollapsed ? (
|
|
924
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
925
|
+
) : (
|
|
926
|
+
<Minus className="h-3.5 w-3.5" />
|
|
927
|
+
)}
|
|
928
|
+
</button>
|
|
929
|
+
<button
|
|
930
|
+
onClick={closeAllTabs}
|
|
931
|
+
className="p-1 rounded hover:bg-muted"
|
|
932
|
+
title="Close all tabs"
|
|
933
|
+
>
|
|
934
|
+
<X className="h-3.5 w-3.5" />
|
|
935
|
+
</button>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
|
|
939
|
+
{/* Active tab content */}
|
|
940
|
+
{!previewCollapsed && activeTabPath && openTabs.has(activeTabPath) && (() => {
|
|
941
|
+
const tab = openTabs.get(activeTabPath)!;
|
|
942
|
+
return (
|
|
943
|
+
<div key={activeTabPath} className="bg-white min-h-24">
|
|
944
|
+
{tab.loading ? (
|
|
945
|
+
<div className="p-3 flex items-center gap-2 text-muted-foreground">
|
|
946
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
947
|
+
<span className="text-sm">Loading file preview...</span>
|
|
948
|
+
</div>
|
|
949
|
+
) : tab.error ? (
|
|
950
|
+
<div className="p-3 text-sm text-destructive flex items-center gap-2">
|
|
951
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
952
|
+
<span>{tab.error}</span>
|
|
953
|
+
</div>
|
|
954
|
+
) : (
|
|
955
|
+
<CodePreview
|
|
956
|
+
code={tab.code}
|
|
957
|
+
compiler={compiler}
|
|
958
|
+
services={namespaces}
|
|
959
|
+
filePath={activeTabPath}
|
|
960
|
+
onOpenEditSession={openSharedEditSession}
|
|
961
|
+
/>
|
|
962
|
+
)}
|
|
963
|
+
</div>
|
|
964
|
+
);
|
|
965
|
+
})()}
|
|
966
|
+
</div>
|
|
967
|
+
)}
|
|
968
|
+
<ScrollArea className="h-full flex-1" ref={scrollRef}>
|
|
969
|
+
<div className="p-4 space-y-4">
|
|
970
|
+
{messages.length === 0 ? (
|
|
971
|
+
<div className="text-center text-muted-foreground py-12">
|
|
972
|
+
<img
|
|
973
|
+
src={APROVAN_LOGO}
|
|
974
|
+
alt=""
|
|
975
|
+
className="h-12 w-12 mx-auto mb-4 opacity-50 rounded-full"
|
|
976
|
+
/>
|
|
977
|
+
<p>Start a conversation</p>
|
|
978
|
+
</div>
|
|
979
|
+
) : (
|
|
980
|
+
messages.map((msg) => (
|
|
981
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
982
|
+
))
|
|
983
|
+
)}
|
|
984
|
+
|
|
985
|
+
{isLoading &&
|
|
986
|
+
messages[messages.length - 1]?.role !== "assistant" && (
|
|
987
|
+
<div className="flex gap-3 justify-start">
|
|
988
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
989
|
+
<img
|
|
990
|
+
src={APROVAN_LOGO}
|
|
991
|
+
alt=""
|
|
992
|
+
className="rounded-full"
|
|
993
|
+
/>
|
|
994
|
+
<AvatarFallback>A</AvatarFallback>
|
|
995
|
+
</Avatar>
|
|
996
|
+
<div className="flex flex-col gap-1">
|
|
997
|
+
<div className="h-5" />
|
|
998
|
+
<div className="bg-muted rounded-lg px-4 py-2">
|
|
999
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
1004
|
+
</div>
|
|
1005
|
+
</ScrollArea>
|
|
1006
|
+
</div>
|
|
1007
|
+
</CardContent>
|
|
1008
|
+
|
|
1009
|
+
{error && (
|
|
1010
|
+
<div className="px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2">
|
|
1011
|
+
<AlertCircle className="h-4 w-4" />
|
|
1012
|
+
{error.message}
|
|
1013
|
+
</div>
|
|
1014
|
+
)}
|
|
1015
|
+
|
|
1016
|
+
<div className="p-4 border-t">
|
|
1017
|
+
<form onSubmit={handleSubmit} className="flex gap-2 items-end">
|
|
1018
|
+
<MarkdownEditor
|
|
1019
|
+
value={input}
|
|
1020
|
+
onChange={setInput}
|
|
1021
|
+
onSubmit={() => {
|
|
1022
|
+
if (!isLoading && input.trim()) {
|
|
1023
|
+
handleSubmit();
|
|
1024
|
+
}
|
|
1025
|
+
}}
|
|
1026
|
+
placeholder="Type a message... (Shift+Enter for new line)"
|
|
1027
|
+
disabled={isLoading}
|
|
1028
|
+
/>
|
|
1029
|
+
<Button
|
|
1030
|
+
type="submit"
|
|
1031
|
+
disabled={isLoading || !input.trim()}
|
|
1032
|
+
className="shrink-0"
|
|
1033
|
+
>
|
|
1034
|
+
{isLoading ? (
|
|
1035
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1036
|
+
) : (
|
|
1037
|
+
<Send className="h-4 w-4" />
|
|
1038
|
+
)}
|
|
1039
|
+
</Button>
|
|
1040
|
+
</form>
|
|
1041
|
+
</div>
|
|
1042
|
+
</Card>
|
|
1043
|
+
|
|
1044
|
+
{!editSession && (
|
|
1045
|
+
<Bobbin
|
|
1046
|
+
container={chatContainer}
|
|
1047
|
+
pillContainer={chatContainer}
|
|
1048
|
+
defaultActive={false}
|
|
1049
|
+
showInspector
|
|
1050
|
+
onChanges={() => undefined}
|
|
1051
|
+
exclude={[".bobbin-pill", "[data-bobbin]"]}
|
|
1052
|
+
/>
|
|
1053
|
+
)}
|
|
1054
|
+
</div>
|
|
1055
|
+
{editSession && (
|
|
1056
|
+
<EditModal
|
|
1057
|
+
isOpen
|
|
1058
|
+
onClose={() => setEditSession(null)}
|
|
1059
|
+
onSaveProject={async (project) => {
|
|
1060
|
+
await saveWorkspaceProject(project);
|
|
1061
|
+
await refreshWorkspace();
|
|
1062
|
+
}}
|
|
1063
|
+
originalProject={editSession.project}
|
|
1064
|
+
initialActiveFile={editSession.initialActiveFile}
|
|
1065
|
+
initialTreePath={editSession.initialTreePath}
|
|
1066
|
+
apiEndpoint="/api/edit"
|
|
1067
|
+
initialState={{ showPreview: true, showTree: true }}
|
|
1068
|
+
compile={async (code) => {
|
|
1069
|
+
if (!compiler) return { success: true };
|
|
1070
|
+
try {
|
|
1071
|
+
await compiler.compile(
|
|
1072
|
+
code,
|
|
1073
|
+
createPreviewManifest(namespaces),
|
|
1074
|
+
{ typescript: true },
|
|
1075
|
+
);
|
|
1076
|
+
return { success: true };
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
return {
|
|
1079
|
+
success: false,
|
|
1080
|
+
error:
|
|
1081
|
+
err instanceof Error ? err.message : "Compilation failed",
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}}
|
|
1085
|
+
renderPreview={(code) => (
|
|
1086
|
+
<WidgetPreview
|
|
1087
|
+
code={code}
|
|
1088
|
+
compiler={compiler}
|
|
1089
|
+
services={namespaces}
|
|
1090
|
+
/>
|
|
1091
|
+
)}
|
|
1092
|
+
previewLoading={!compiler}
|
|
1093
|
+
/>
|
|
1094
|
+
)}
|
|
1095
|
+
</SharedEditSessionCtx.Provider>
|
|
1096
|
+
</PatchworkCtx.Provider>
|
|
1097
|
+
);
|
|
1098
|
+
}
|