@aprovan/patchwork-chat 0.1.0-dev.6bd527d → 0.1.0-dev.f456953
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 +303 -6
- package/.utcp_config.json +17 -1
- 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-Dv7Oe6Be.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-CMjwMIkn.js +1 -0
- package/dist/assets/ayu-light-C47S-Tmv.js +1 -0
- package/dist/assets/ayu-mirage-CjoLj4QM.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/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-VCDPK7BO.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-CgJxNhIT.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/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-Cjs5j4Ri.js +1490 -0
- package/dist/assets/index-jHPB7pV3.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/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-DGMBWnxU.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-BtvRca6l.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-DZsq8hO1.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-DUICxH0z.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-B93PlW-d.js +1 -0
- package/dist/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/assets/moonbit-Ba13S78F.js +1 -0
- package/dist/assets/move-IF9eRakj.js +1 -0
- package/dist/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/assets/nextflow-BrzmwbiE.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-C-sUppwS.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-BhRPY-oY.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-D5oM4XIm.js +1 -0
- package/dist/assets/ruby-Cw6WdidG.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-zxCyuUbr.js +1 -0
- package/dist/assets/swift-Dg5xB15N.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-CvyZ59Mk.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-ChbOoGGc.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 +2 -2
- package/package.json +6 -6
- package/src/lib/workspace-vfs.ts +185 -0
- package/src/pages/ChatPage.tsx +770 -180
- package/.working/widgets/27060b91-a2a5-4272-b243-6eb904bd4070/main.tsx +0 -107
- package/dist/assets/index-Ct0GSTdJ.css +0 -1
- package/dist/assets/index-ueH8ysw1.js +0 -1455
package/src/pages/ChatPage.tsx
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { useChat } from
|
|
2
|
-
import { DefaultChatTransport } from
|
|
3
|
-
import {
|
|
1
|
+
import { useChat } from "@ai-sdk/react";
|
|
2
|
+
import { DefaultChatTransport } from "ai";
|
|
3
|
+
import {
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useCallback,
|
|
9
|
+
createContext,
|
|
10
|
+
useContext,
|
|
11
|
+
} from "react";
|
|
4
12
|
import {
|
|
5
13
|
Send,
|
|
6
14
|
Loader2,
|
|
@@ -8,75 +16,91 @@ import {
|
|
|
8
16
|
AlertCircle,
|
|
9
17
|
Brain,
|
|
10
18
|
ChevronDown,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
19
|
+
Minus,
|
|
20
|
+
RefreshCw,
|
|
21
|
+
X,
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import { Bobbin } from '@aprovan/bobbin';
|
|
24
|
+
import { Button } from "@/components/ui/button";
|
|
25
|
+
import { Input } from "@/components/ui/input";
|
|
26
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
27
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
28
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
29
|
+
import { Badge } from "@/components/ui/badge";
|
|
17
30
|
import {
|
|
18
31
|
Collapsible,
|
|
19
32
|
CollapsibleContent,
|
|
20
33
|
CollapsibleTrigger,
|
|
21
|
-
} from
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
} from "@/components/ui/collapsible";
|
|
35
|
+
import {
|
|
36
|
+
Dialog,
|
|
37
|
+
DialogHeader,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogClose,
|
|
40
|
+
} from "@/components/ui/dialog";
|
|
41
|
+
import Markdown from "react-markdown";
|
|
42
|
+
import remarkGfm from "remark-gfm";
|
|
43
|
+
import type { UIMessage } from "ai";
|
|
44
|
+
import { createCompiler, type Compiler } from "@aprovan/patchwork-compiler";
|
|
26
45
|
import {
|
|
27
46
|
extractCodeBlocks,
|
|
28
47
|
CodePreview,
|
|
48
|
+
WidgetPreview,
|
|
29
49
|
MarkdownEditor,
|
|
30
50
|
ServicesInspector,
|
|
51
|
+
EditModal,
|
|
52
|
+
FileTree,
|
|
31
53
|
type ServiceInfo,
|
|
32
|
-
} from
|
|
54
|
+
} from "@aprovan/patchwork-editor";
|
|
55
|
+
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
56
|
+
import {
|
|
57
|
+
listWorkspaceEntries,
|
|
58
|
+
listWorkspacePaths,
|
|
59
|
+
toWorkspaceTreeFiles,
|
|
60
|
+
loadWorkspaceDirectoryProject,
|
|
61
|
+
loadWorkspaceFileProject,
|
|
62
|
+
createSingleWorkspaceFileProject,
|
|
63
|
+
saveWorkspaceProject,
|
|
64
|
+
subscribeToWorkspaceChanges,
|
|
65
|
+
} from "@/lib/workspace-vfs";
|
|
33
66
|
|
|
34
67
|
const APROVAN_LOGO =
|
|
35
|
-
|
|
68
|
+
"https://raw.githubusercontent.com/AprovanLabs/aprovan.com/main/docs/assets/social-labs.png";
|
|
36
69
|
|
|
37
70
|
interface PatchworkContext {
|
|
38
71
|
compiler: Compiler | null;
|
|
39
72
|
namespaces: string[];
|
|
40
73
|
}
|
|
41
74
|
|
|
42
|
-
const PatchworkCtx = createContext<PatchworkContext>({
|
|
75
|
+
const PatchworkCtx = createContext<PatchworkContext>({
|
|
76
|
+
compiler: null,
|
|
77
|
+
namespaces: [],
|
|
78
|
+
});
|
|
43
79
|
const useCompiler = () => useContext(PatchworkCtx).compiler;
|
|
44
80
|
const useServices = () => useContext(PatchworkCtx).namespaces;
|
|
45
81
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
82
|
+
function createPreviewManifest(services?: string[]) {
|
|
83
|
+
return {
|
|
84
|
+
name: "preview",
|
|
85
|
+
version: "1.0.0",
|
|
86
|
+
platform: "browser" as const,
|
|
87
|
+
image: "@aprovan/patchwork-image-shadcn",
|
|
88
|
+
services,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
const SharedEditSessionCtx = createContext<
|
|
93
|
+
| ((session: {
|
|
94
|
+
projectId: string;
|
|
95
|
+
entryFile: string;
|
|
96
|
+
filePath?: string;
|
|
97
|
+
initialCode: string;
|
|
98
|
+
initialProject: VirtualProject;
|
|
99
|
+
}) => void)
|
|
100
|
+
| null
|
|
101
|
+
>(null);
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<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">
|
|
62
|
-
{parts.map((part, index) => {
|
|
63
|
-
if (part.type === 'code') {
|
|
64
|
-
return (
|
|
65
|
-
<CodePreview
|
|
66
|
-
key={index}
|
|
67
|
-
code={part.content}
|
|
68
|
-
compiler={compiler}
|
|
69
|
-
services={services}
|
|
70
|
-
filePath={part.attributes?.path}
|
|
71
|
-
entrypoint="main.tsx"
|
|
72
|
-
/>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return <Markdown key={index} remarkPlugins={[remarkGfm]}>{part.content}</Markdown>;
|
|
76
|
-
})}
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
103
|
+
const useSharedEditSession = () => useContext(SharedEditSessionCtx);
|
|
80
104
|
|
|
81
105
|
function ReasoningPart({
|
|
82
106
|
text,
|
|
@@ -117,8 +141,8 @@ function ToolPart({
|
|
|
117
141
|
output?: unknown;
|
|
118
142
|
errorText?: string;
|
|
119
143
|
}) {
|
|
120
|
-
const isRunning = state ===
|
|
121
|
-
const hasError = state ===
|
|
144
|
+
const isRunning = state === "input-streaming" || state === "input-available";
|
|
145
|
+
const hasError = state === "output-error";
|
|
122
146
|
|
|
123
147
|
return (
|
|
124
148
|
<Collapsible className="my-1 w-full">
|
|
@@ -142,7 +166,7 @@ function ToolPart({
|
|
|
142
166
|
</span>
|
|
143
167
|
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
144
168
|
<pre className="whitespace-pre-wrap break-words m-0">
|
|
145
|
-
{typeof input ===
|
|
169
|
+
{typeof input === "string"
|
|
146
170
|
? input
|
|
147
171
|
: JSON.stringify(input, null, 2)}
|
|
148
172
|
</pre>
|
|
@@ -157,7 +181,7 @@ function ToolPart({
|
|
|
157
181
|
</span>
|
|
158
182
|
<div className="mt-1 p-2 bg-muted/30 rounded text-xs overflow-auto max-h-48">
|
|
159
183
|
<pre className="whitespace-pre-wrap break-words m-0">
|
|
160
|
-
{typeof output ===
|
|
184
|
+
{typeof output === "string"
|
|
161
185
|
? output
|
|
162
186
|
: JSON.stringify(output, null, 2)}
|
|
163
187
|
</pre>
|
|
@@ -177,22 +201,18 @@ function ToolPart({
|
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
function MessageBubble({ message }: { message: UIMessage }) {
|
|
180
|
-
const isUser = message.role ===
|
|
204
|
+
const isUser = message.role === "user";
|
|
181
205
|
const isStreaming = message.parts?.some(
|
|
182
206
|
(p) =>
|
|
183
|
-
|
|
184
|
-
(p.state ===
|
|
207
|
+
"state" in p &&
|
|
208
|
+
(p.state === "input-streaming" || p.state === "input-available"),
|
|
185
209
|
);
|
|
186
210
|
|
|
187
211
|
return (
|
|
188
|
-
<div className={`flex gap-3 ${isUser ?
|
|
212
|
+
<div className={`flex gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
|
|
189
213
|
{!isUser && (
|
|
190
214
|
<Avatar className="h-8 w-8 shrink-0">
|
|
191
|
-
<img
|
|
192
|
-
src={APROVAN_LOGO}
|
|
193
|
-
alt="Assistant"
|
|
194
|
-
className="rounded-full"
|
|
195
|
-
/>
|
|
215
|
+
<img src={APROVAN_LOGO} alt="Assistant" className="rounded-full" />
|
|
196
216
|
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
197
217
|
A
|
|
198
218
|
</AvatarFallback>
|
|
@@ -201,7 +221,7 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
201
221
|
|
|
202
222
|
<div
|
|
203
223
|
className={`flex flex-col gap-1 max-w-[80%] min-w-0 ${
|
|
204
|
-
isUser ?
|
|
224
|
+
isUser ? "items-end" : "items-start"
|
|
205
225
|
}`}
|
|
206
226
|
>
|
|
207
227
|
<div className="flex items-center gap-2 h-5">
|
|
@@ -209,10 +229,7 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
209
229
|
{message.role}
|
|
210
230
|
</span>
|
|
211
231
|
{isStreaming && (
|
|
212
|
-
<Badge
|
|
213
|
-
variant="outline"
|
|
214
|
-
className="text-xs"
|
|
215
|
-
>
|
|
232
|
+
<Badge variant="outline" className="text-xs">
|
|
216
233
|
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
|
217
234
|
streaming
|
|
218
235
|
</Badge>
|
|
@@ -221,31 +238,27 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
221
238
|
|
|
222
239
|
<div
|
|
223
240
|
className={`rounded-lg px-4 py-2 overflow-hidden w-full ${
|
|
224
|
-
isUser ?
|
|
241
|
+
isUser ? "bg-primary text-primary-foreground" : "bg-muted"
|
|
225
242
|
}`}
|
|
226
243
|
>
|
|
227
244
|
{message.parts?.map((part, i) => {
|
|
228
|
-
if (part.type ===
|
|
245
|
+
if (part.type === "text") {
|
|
229
246
|
return (
|
|
230
|
-
<
|
|
231
|
-
key={i}
|
|
232
|
-
text={part.text}
|
|
233
|
-
isUser={isUser}
|
|
234
|
-
/>
|
|
247
|
+
<TextPartWithSession key={i} text={part.text} isUser={isUser} />
|
|
235
248
|
);
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
if (part.type ===
|
|
251
|
+
if (part.type === "reasoning") {
|
|
239
252
|
return (
|
|
240
253
|
<ReasoningPart
|
|
241
254
|
key={i}
|
|
242
255
|
text={part.text}
|
|
243
|
-
isStreaming={part.state ===
|
|
256
|
+
isStreaming={part.state === "streaming"}
|
|
244
257
|
/>
|
|
245
258
|
);
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
if (part.type.startsWith(
|
|
261
|
+
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
|
|
249
262
|
const toolPart = part as {
|
|
250
263
|
type: string;
|
|
251
264
|
toolName?: string;
|
|
@@ -256,7 +269,7 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
256
269
|
errorText?: string;
|
|
257
270
|
};
|
|
258
271
|
const toolName =
|
|
259
|
-
toolPart.toolName ?? part.type.replace(
|
|
272
|
+
toolPart.toolName ?? part.type.replace("tool-", "");
|
|
260
273
|
return (
|
|
261
274
|
<ToolPart
|
|
262
275
|
key={i}
|
|
@@ -283,22 +296,209 @@ function MessageBubble({ message }: { message: UIMessage }) {
|
|
|
283
296
|
);
|
|
284
297
|
}
|
|
285
298
|
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
function TextPartWithSession({
|
|
300
|
+
text,
|
|
301
|
+
isUser,
|
|
302
|
+
}: {
|
|
303
|
+
text: string;
|
|
304
|
+
isUser: boolean;
|
|
305
|
+
}) {
|
|
306
|
+
const open = useSharedEditSession();
|
|
307
|
+
const compiler = useCompiler();
|
|
308
|
+
const services = useServices();
|
|
309
|
+
|
|
310
|
+
if (isUser) {
|
|
311
|
+
return (
|
|
312
|
+
<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">
|
|
313
|
+
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const parts = extractCodeBlocks(text);
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<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">
|
|
322
|
+
{parts.map((part, index) => {
|
|
323
|
+
if (part.type === "code") {
|
|
324
|
+
return (
|
|
325
|
+
<CodePreview
|
|
326
|
+
key={index}
|
|
327
|
+
code={part.content}
|
|
328
|
+
compiler={compiler}
|
|
329
|
+
services={services}
|
|
330
|
+
filePath={part.attributes?.path}
|
|
331
|
+
entrypoint="main.tsx"
|
|
332
|
+
onOpenEditSession={open ?? undefined}
|
|
333
|
+
/>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return (
|
|
337
|
+
<Markdown key={index} remarkPlugins={[remarkGfm]}>
|
|
338
|
+
{part.content}
|
|
339
|
+
</Markdown>
|
|
340
|
+
);
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const PROXY_URL = "/api/proxy";
|
|
347
|
+
const IMAGE_SPEC = "@aprovan/patchwork-image-shadcn";
|
|
288
348
|
// Local proxy for loading image packages, esm.sh for widget imports
|
|
289
|
-
const IMAGE_CDN_URL = import.meta.env.DEV
|
|
290
|
-
|
|
349
|
+
const IMAGE_CDN_URL = import.meta.env.DEV
|
|
350
|
+
? "/_local-packages"
|
|
351
|
+
: "https://esm.sh";
|
|
352
|
+
const WIDGET_CDN_URL = "https://esm.sh"; // Widget imports need esm.sh bundles like @packagedcn
|
|
353
|
+
|
|
354
|
+
function toProjectRelativePath(projectId: string, path: string): string {
|
|
355
|
+
const normalizedProjectId = projectId.replace(/^\/+|\/+$/g, "");
|
|
356
|
+
const normalizedPath = path.replace(/^\/+|\/+$/g, "");
|
|
357
|
+
if (!normalizedProjectId) return normalizedPath;
|
|
358
|
+
const prefix = `${normalizedProjectId}/`;
|
|
359
|
+
if (normalizedPath.startsWith(prefix)) {
|
|
360
|
+
return normalizedPath.slice(prefix.length);
|
|
361
|
+
}
|
|
362
|
+
return normalizedPath;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const TABS_STORAGE_KEY = 'patchwork:open-tabs';
|
|
366
|
+
|
|
367
|
+
function loadPersistedTabState(): { paths: string[]; activePath: string | null } {
|
|
368
|
+
try {
|
|
369
|
+
const raw = localStorage.getItem(TABS_STORAGE_KEY);
|
|
370
|
+
if (!raw) return { paths: [], activePath: null };
|
|
371
|
+
const parsed = JSON.parse(raw);
|
|
372
|
+
return {
|
|
373
|
+
paths: Array.isArray(parsed.paths) ? parsed.paths : [],
|
|
374
|
+
activePath: typeof parsed.activePath === 'string' ? parsed.activePath : null,
|
|
375
|
+
};
|
|
376
|
+
} catch {
|
|
377
|
+
return { paths: [], activePath: null };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function persistTabState(paths: string[], activePath: string | null) {
|
|
382
|
+
localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify({ paths, activePath }));
|
|
383
|
+
}
|
|
291
384
|
|
|
292
385
|
export default function ChatPage() {
|
|
293
|
-
const [input, setInput] = useState(
|
|
386
|
+
const [input, setInput] = useState(
|
|
387
|
+
"What's the weather in Houston, Texas like?",
|
|
388
|
+
);
|
|
294
389
|
const [compiler, setCompiler] = useState<Compiler | null>(null);
|
|
295
390
|
const [namespaces, setNamespaces] = useState<string[]>([]);
|
|
296
391
|
const [services, setServices] = useState<ServiceInfo[]>([]);
|
|
392
|
+
const [workspaceFiles, setWorkspaceFiles] = useState<string[]>([]);
|
|
393
|
+
const [workspaceActivePath, setWorkspaceActivePath] = useState("");
|
|
394
|
+
const [workspaceFilter, setWorkspaceFilter] = useState("");
|
|
395
|
+
const [workspaceTreeVersion, setWorkspaceTreeVersion] = useState(0);
|
|
396
|
+
const [workspaceLoading, setWorkspaceLoading] = useState(false);
|
|
397
|
+
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
|
|
398
|
+
const [chatContainer, setChatContainer] = useState<HTMLDivElement | null>(
|
|
399
|
+
null,
|
|
400
|
+
);
|
|
401
|
+
const [editSession, setEditSession] = useState<{
|
|
402
|
+
project: VirtualProject;
|
|
403
|
+
initialTreePath?: string;
|
|
404
|
+
initialActiveFile?: string;
|
|
405
|
+
} | null>(null);
|
|
406
|
+
const [openTabs, setOpenTabs] = useState<
|
|
407
|
+
Map<string, { code: string; loading: boolean; error: string | null }>
|
|
408
|
+
>(() => {
|
|
409
|
+
const { paths } = loadPersistedTabState();
|
|
410
|
+
return new Map(paths.map((p) => [p, { code: '', loading: true, error: null }]));
|
|
411
|
+
});
|
|
412
|
+
const [activeTabPath, setActiveTabPath] = useState<string | null>(() => {
|
|
413
|
+
const { paths, activePath } = loadPersistedTabState();
|
|
414
|
+
if (activePath && paths.includes(activePath)) return activePath;
|
|
415
|
+
return paths[0] ?? null;
|
|
416
|
+
});
|
|
417
|
+
const [previewCollapsed, setPreviewCollapsed] = useState(false);
|
|
297
418
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
419
|
+
const tabRequestRefs = useRef<Map<string, number>>(new Map());
|
|
420
|
+
|
|
421
|
+
const [pinnedPaths, setPinnedPaths] = useState<Map<string, boolean>>(() => {
|
|
422
|
+
try {
|
|
423
|
+
const stored = localStorage.getItem('patchwork:pinned-paths');
|
|
424
|
+
if (!stored) return new Map();
|
|
425
|
+
const parsed = JSON.parse(stored) as Array<[string, boolean]> | string[];
|
|
426
|
+
if (parsed.length > 0 && Array.isArray(parsed[0])) {
|
|
427
|
+
return new Map(parsed as Array<[string, boolean]>);
|
|
428
|
+
}
|
|
429
|
+
return new Map((parsed as string[]).map((p) => [p, false]));
|
|
430
|
+
} catch {
|
|
431
|
+
return new Map();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const togglePin = useCallback((path: string, isDir: boolean) => {
|
|
436
|
+
setPinnedPaths((prev) => {
|
|
437
|
+
const next = new Map(prev);
|
|
438
|
+
if (next.has(path)) next.delete(path);
|
|
439
|
+
else next.set(path, isDir);
|
|
440
|
+
localStorage.setItem('patchwork:pinned-paths', JSON.stringify(Array.from(next)));
|
|
441
|
+
return next;
|
|
442
|
+
});
|
|
443
|
+
}, []);
|
|
444
|
+
|
|
445
|
+
const refreshWorkspace = useCallback(async () => {
|
|
446
|
+
setWorkspaceLoading(true);
|
|
447
|
+
setWorkspaceError(null);
|
|
448
|
+
try {
|
|
449
|
+
if (workspaceFilter.trim()) {
|
|
450
|
+
const paths = await listWorkspacePaths();
|
|
451
|
+
setWorkspaceFiles(paths);
|
|
452
|
+
}
|
|
453
|
+
setWorkspaceTreeVersion((prev) => prev + 1);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
setWorkspaceError(
|
|
456
|
+
err instanceof Error ? err.message : "Failed to load workspace",
|
|
457
|
+
);
|
|
458
|
+
} finally {
|
|
459
|
+
setWorkspaceLoading(false);
|
|
460
|
+
}
|
|
461
|
+
}, [workspaceFilter]);
|
|
462
|
+
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
return subscribeToWorkspaceChanges(() => {
|
|
465
|
+
setWorkspaceTreeVersion((prev) => prev + 1);
|
|
466
|
+
listWorkspacePaths()
|
|
467
|
+
.then((allPaths) => {
|
|
468
|
+
if (workspaceFilter.trim()) setWorkspaceFiles(allPaths);
|
|
469
|
+
const existing = new Set(allPaths);
|
|
470
|
+
setOpenTabs((prev) => {
|
|
471
|
+
let changed = false;
|
|
472
|
+
const next = new Map(prev);
|
|
473
|
+
for (const path of next.keys()) {
|
|
474
|
+
if (!existing.has(path)) { next.delete(path); changed = true; }
|
|
475
|
+
}
|
|
476
|
+
return changed ? next : prev;
|
|
477
|
+
});
|
|
478
|
+
})
|
|
479
|
+
.catch(() => {});
|
|
480
|
+
});
|
|
481
|
+
}, [workspaceFilter]);
|
|
482
|
+
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (!workspaceFilter.trim()) return;
|
|
485
|
+
|
|
486
|
+
setWorkspaceLoading(true);
|
|
487
|
+
setWorkspaceError(null);
|
|
488
|
+
|
|
489
|
+
listWorkspacePaths()
|
|
490
|
+
.then(setWorkspaceFiles)
|
|
491
|
+
.catch((err) => {
|
|
492
|
+
setWorkspaceError(
|
|
493
|
+
err instanceof Error ? err.message : "Failed to load workspace",
|
|
494
|
+
);
|
|
495
|
+
})
|
|
496
|
+
.finally(() => setWorkspaceLoading(false));
|
|
497
|
+
}, [workspaceFilter]);
|
|
298
498
|
|
|
299
499
|
useEffect(() => {
|
|
300
500
|
// Fetch available services
|
|
301
|
-
fetch(
|
|
501
|
+
fetch("/api/services")
|
|
302
502
|
.then((res) => res.json())
|
|
303
503
|
.then((data) => {
|
|
304
504
|
setNamespaces(data.namespaces ?? []);
|
|
@@ -313,17 +513,191 @@ export default function ChatPage() {
|
|
|
313
513
|
});
|
|
314
514
|
|
|
315
515
|
// Initialize compiler
|
|
316
|
-
createCompiler({
|
|
317
|
-
image: IMAGE_SPEC,
|
|
318
|
-
proxyUrl: PROXY_URL,
|
|
516
|
+
createCompiler({
|
|
517
|
+
image: IMAGE_SPEC,
|
|
518
|
+
proxyUrl: PROXY_URL,
|
|
319
519
|
cdnBaseUrl: IMAGE_CDN_URL,
|
|
320
520
|
widgetCdnBaseUrl: WIDGET_CDN_URL,
|
|
321
521
|
})
|
|
322
522
|
.then(setCompiler)
|
|
323
523
|
.catch(console.error);
|
|
524
|
+
|
|
525
|
+
void refreshWorkspace();
|
|
526
|
+
}, []);
|
|
527
|
+
|
|
528
|
+
// Load content for tabs restored from localStorage
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
openTabs.forEach((tab, path) => {
|
|
531
|
+
if (!tab.loading) return;
|
|
532
|
+
const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
|
|
533
|
+
tabRequestRefs.current.set(path, requestId);
|
|
534
|
+
loadWorkspaceFileProject(path)
|
|
535
|
+
.then((project) => {
|
|
536
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
537
|
+
if (!project) {
|
|
538
|
+
setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const file = project.files.get(project.entry);
|
|
542
|
+
setOpenTabs((prev) => {
|
|
543
|
+
const next = new Map(prev);
|
|
544
|
+
next.set(path, { code: file?.content ?? '', loading: false, error: null });
|
|
545
|
+
return next;
|
|
546
|
+
});
|
|
547
|
+
})
|
|
548
|
+
.catch(() => {
|
|
549
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
550
|
+
setOpenTabs((prev) => { const next = new Map(prev); next.delete(path); return next; });
|
|
551
|
+
});
|
|
552
|
+
});
|
|
324
553
|
}, []);
|
|
325
554
|
|
|
326
|
-
|
|
555
|
+
// Persist open tabs to localStorage
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
persistTabState([...openTabs.keys()], activeTabPath);
|
|
558
|
+
}, [openTabs, activeTabPath]);
|
|
559
|
+
|
|
560
|
+
// Fix activeTabPath when its tab is removed
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
if (activeTabPath !== null && !openTabs.has(activeTabPath)) {
|
|
563
|
+
setActiveTabPath([...openTabs.keys()][0] ?? null);
|
|
564
|
+
}
|
|
565
|
+
}, [openTabs, activeTabPath]);
|
|
566
|
+
|
|
567
|
+
const openSharedEditSession = useCallback(
|
|
568
|
+
async (session: {
|
|
569
|
+
projectId: string;
|
|
570
|
+
entryFile: string;
|
|
571
|
+
filePath?: string;
|
|
572
|
+
initialCode: string;
|
|
573
|
+
initialProject: VirtualProject;
|
|
574
|
+
}) => {
|
|
575
|
+
const { projectId, filePath, entryFile, initialCode, initialProject } =
|
|
576
|
+
session;
|
|
577
|
+
const directoryProject = await loadWorkspaceDirectoryProject(projectId);
|
|
578
|
+
const filePathKey = filePath ?? `${projectId}/${entryFile}`;
|
|
579
|
+
|
|
580
|
+
if (directoryProject) {
|
|
581
|
+
const relativePath = toProjectRelativePath(projectId, filePathKey);
|
|
582
|
+
setWorkspaceActivePath(filePathKey);
|
|
583
|
+
setEditSession({
|
|
584
|
+
project: directoryProject,
|
|
585
|
+
initialTreePath: relativePath,
|
|
586
|
+
initialActiveFile: relativePath,
|
|
587
|
+
});
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const fallbackFilePath = filePathKey;
|
|
592
|
+
const fallbackProject = filePath
|
|
593
|
+
? createSingleWorkspaceFileProject(filePath, initialCode)
|
|
594
|
+
: initialProject;
|
|
595
|
+
setWorkspaceActivePath(fallbackFilePath);
|
|
596
|
+
setEditSession({
|
|
597
|
+
project: fallbackProject,
|
|
598
|
+
initialTreePath: fallbackProject.entry,
|
|
599
|
+
initialActiveFile: fallbackProject.entry,
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
[],
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const openWorkspaceSession = useCallback(
|
|
606
|
+
async (path: string, isDir: boolean) => {
|
|
607
|
+
const project = isDir
|
|
608
|
+
? await loadWorkspaceDirectoryProject(path)
|
|
609
|
+
: await loadWorkspaceFileProject(path);
|
|
610
|
+
if (!project) return;
|
|
611
|
+
|
|
612
|
+
setWorkspaceActivePath(path);
|
|
613
|
+
setEditSession({
|
|
614
|
+
project,
|
|
615
|
+
initialTreePath: project.entry,
|
|
616
|
+
initialActiveFile: project.entry,
|
|
617
|
+
});
|
|
618
|
+
},
|
|
619
|
+
[],
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const openWorkspacePreview = useCallback((path: string) => {
|
|
623
|
+
setWorkspaceActivePath(path);
|
|
624
|
+
setActiveTabPath(path);
|
|
625
|
+
setPreviewCollapsed(false);
|
|
626
|
+
|
|
627
|
+
// If tab already open, just activate it
|
|
628
|
+
setOpenTabs((prev) => {
|
|
629
|
+
if (prev.has(path)) return prev;
|
|
630
|
+
const next = new Map(prev);
|
|
631
|
+
next.set(path, { code: "", loading: true, error: null });
|
|
632
|
+
return next;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const requestId = (tabRequestRefs.current.get(path) ?? 0) + 1;
|
|
636
|
+
tabRequestRefs.current.set(path, requestId);
|
|
637
|
+
|
|
638
|
+
void loadWorkspaceFileProject(path)
|
|
639
|
+
.then((project) => {
|
|
640
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
641
|
+
if (!project) {
|
|
642
|
+
setOpenTabs((prev) => {
|
|
643
|
+
const next = new Map(prev);
|
|
644
|
+
next.set(path, { code: "", loading: false, error: "Failed to load file preview" });
|
|
645
|
+
return next;
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const file = project.files.get(project.entry);
|
|
650
|
+
setOpenTabs((prev) => {
|
|
651
|
+
const next = new Map(prev);
|
|
652
|
+
next.set(path, { code: file?.content ?? "", loading: false, error: null });
|
|
653
|
+
return next;
|
|
654
|
+
});
|
|
655
|
+
})
|
|
656
|
+
.catch((err) => {
|
|
657
|
+
if (tabRequestRefs.current.get(path) !== requestId) return;
|
|
658
|
+
setOpenTabs((prev) => {
|
|
659
|
+
const next = new Map(prev);
|
|
660
|
+
next.set(path, {
|
|
661
|
+
code: "",
|
|
662
|
+
loading: false,
|
|
663
|
+
error: err instanceof Error ? err.message : "Failed to load file preview",
|
|
664
|
+
});
|
|
665
|
+
return next;
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
}, []);
|
|
669
|
+
|
|
670
|
+
const closeTab = useCallback((path: string) => {
|
|
671
|
+
setOpenTabs((prev) => {
|
|
672
|
+
const next = new Map(prev);
|
|
673
|
+
next.delete(path);
|
|
674
|
+
return next;
|
|
675
|
+
});
|
|
676
|
+
setActiveTabPath((prev) => {
|
|
677
|
+
if (prev !== path) return prev;
|
|
678
|
+
// Activate an adjacent tab
|
|
679
|
+
const paths = [...openTabs.keys()];
|
|
680
|
+
const idx = paths.indexOf(path);
|
|
681
|
+
if (paths.length <= 1) return null;
|
|
682
|
+
return paths[idx > 0 ? idx - 1 : idx + 1] ?? null;
|
|
683
|
+
});
|
|
684
|
+
}, [openTabs]);
|
|
685
|
+
|
|
686
|
+
const closeAllTabs = useCallback(() => {
|
|
687
|
+
setOpenTabs(new Map());
|
|
688
|
+
setActiveTabPath(null);
|
|
689
|
+
}, []);
|
|
690
|
+
|
|
691
|
+
const filteredWorkspaceFiles = useMemo(() => {
|
|
692
|
+
const query = workspaceFilter.trim().toLowerCase();
|
|
693
|
+
if (!query) return workspaceFiles;
|
|
694
|
+
return workspaceFiles.filter((path) => path.toLowerCase().includes(query));
|
|
695
|
+
}, [workspaceFiles, workspaceFilter]);
|
|
696
|
+
|
|
697
|
+
const patchworkCtx = useMemo(
|
|
698
|
+
() => ({ compiler, namespaces }),
|
|
699
|
+
[compiler, namespaces],
|
|
700
|
+
);
|
|
327
701
|
|
|
328
702
|
const transport = useMemo(
|
|
329
703
|
() =>
|
|
@@ -339,123 +713,339 @@ export default function ChatPage() {
|
|
|
339
713
|
|
|
340
714
|
const { messages, sendMessage, status, error } = useChat({ transport });
|
|
341
715
|
|
|
342
|
-
const isLoading = status ===
|
|
716
|
+
const isLoading = status === "submitted" || status === "streaming";
|
|
343
717
|
|
|
344
|
-
const handleSubmit = useCallback(
|
|
345
|
-
e
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
718
|
+
const handleSubmit = useCallback(
|
|
719
|
+
(e?: React.FormEvent) => {
|
|
720
|
+
e?.preventDefault();
|
|
721
|
+
if (!input.trim()) return;
|
|
722
|
+
sendMessage({ text: input });
|
|
723
|
+
setInput("");
|
|
724
|
+
},
|
|
725
|
+
[input, sendMessage],
|
|
726
|
+
);
|
|
350
727
|
|
|
351
728
|
useEffect(() => {
|
|
352
729
|
scrollRef.current?.scrollTo({
|
|
353
730
|
top: scrollRef.current.scrollHeight,
|
|
354
|
-
behavior:
|
|
731
|
+
behavior: "smooth",
|
|
355
732
|
});
|
|
356
733
|
}, [messages]);
|
|
357
734
|
|
|
358
735
|
return (
|
|
359
736
|
<PatchworkCtx.Provider value={patchworkCtx}>
|
|
360
|
-
<
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
737
|
+
<SharedEditSessionCtx.Provider value={openSharedEditSession}>
|
|
738
|
+
<div
|
|
739
|
+
className="flex flex-col h-screen max-w-6xl mx-auto p-4"
|
|
740
|
+
ref={setChatContainer}
|
|
741
|
+
>
|
|
742
|
+
<Card className="flex-1 flex flex-col min-h-0 overflow-hidden border">
|
|
743
|
+
<CardHeader className="border-b py-3">
|
|
744
|
+
<CardTitle className="flex items-center gap-3">
|
|
745
|
+
<img
|
|
746
|
+
src={APROVAN_LOGO}
|
|
747
|
+
alt="Aprovan"
|
|
748
|
+
className="h-8 w-8 rounded-full"
|
|
749
|
+
/>
|
|
750
|
+
<span className="text-lg">patchwork</span>
|
|
751
|
+
<ServicesInspector
|
|
752
|
+
namespaces={namespaces}
|
|
753
|
+
services={services}
|
|
754
|
+
DialogComponent={({ open, onOpenChange, children }) => (
|
|
755
|
+
<Dialog
|
|
756
|
+
open={open ?? false}
|
|
757
|
+
onOpenChange={onOpenChange ?? (() => undefined)}
|
|
758
|
+
>
|
|
759
|
+
{children}
|
|
760
|
+
</Dialog>
|
|
761
|
+
)}
|
|
762
|
+
DialogHeaderComponent={DialogHeader}
|
|
763
|
+
DialogContentComponent={DialogContent}
|
|
764
|
+
DialogCloseComponent={({ onClose }) => (
|
|
765
|
+
<DialogClose onClose={onClose ?? (() => undefined)} />
|
|
766
|
+
)}
|
|
767
|
+
/>
|
|
768
|
+
</CardTitle>
|
|
769
|
+
</CardHeader>
|
|
770
|
+
|
|
771
|
+
<CardContent className="flex-1 p-0 min-h-0 flex">
|
|
772
|
+
<div className="w-64 border-r bg-muted/20 min-h-0 flex flex-col">
|
|
773
|
+
<div className="px-3 py-2 border-b flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
774
|
+
<span>Workspace</span>
|
|
775
|
+
<button
|
|
776
|
+
onClick={() => void refreshWorkspace()}
|
|
777
|
+
className="ml-auto p-1 rounded hover:bg-muted"
|
|
778
|
+
title="Refresh workspace"
|
|
779
|
+
>
|
|
780
|
+
<RefreshCw
|
|
781
|
+
className={`h-3 w-3 ${
|
|
782
|
+
workspaceLoading ? "animate-spin" : ""
|
|
783
|
+
}`}
|
|
386
784
|
/>
|
|
387
|
-
|
|
785
|
+
</button>
|
|
786
|
+
</div>
|
|
787
|
+
<div className="p-2 border-b">
|
|
788
|
+
<Input
|
|
789
|
+
value={workspaceFilter}
|
|
790
|
+
onChange={(e) => setWorkspaceFilter(e.target.value)}
|
|
791
|
+
placeholder="Filter files..."
|
|
792
|
+
className="h-8"
|
|
793
|
+
/>
|
|
794
|
+
</div>
|
|
795
|
+
{workspaceError ? (
|
|
796
|
+
<div className="p-3 text-xs text-destructive">
|
|
797
|
+
{workspaceError}
|
|
388
798
|
</div>
|
|
799
|
+
) : workspaceFilter.trim() ? (
|
|
800
|
+
<FileTree
|
|
801
|
+
files={toWorkspaceTreeFiles(filteredWorkspaceFiles)}
|
|
802
|
+
activePath={workspaceActivePath}
|
|
803
|
+
onSelectFile={openWorkspacePreview}
|
|
804
|
+
onSelectDirectory={setWorkspaceActivePath}
|
|
805
|
+
onOpenInEditor={openWorkspaceSession}
|
|
806
|
+
openInEditorMode="all"
|
|
807
|
+
openInEditorTitle="Edit"
|
|
808
|
+
pinnedPaths={pinnedPaths}
|
|
809
|
+
onTogglePin={togglePin}
|
|
810
|
+
title="Files"
|
|
811
|
+
/>
|
|
389
812
|
) : (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
813
|
+
<FileTree
|
|
814
|
+
files={[]}
|
|
815
|
+
activePath={workspaceActivePath}
|
|
816
|
+
onSelectFile={openWorkspacePreview}
|
|
817
|
+
onSelectDirectory={setWorkspaceActivePath}
|
|
818
|
+
onOpenInEditor={openWorkspaceSession}
|
|
819
|
+
openInEditorMode="all"
|
|
820
|
+
openInEditorTitle="Edit"
|
|
821
|
+
directoryLoader={listWorkspaceEntries}
|
|
822
|
+
pageSize={10}
|
|
823
|
+
reloadToken={workspaceTreeVersion}
|
|
824
|
+
pinnedPaths={pinnedPaths}
|
|
825
|
+
onTogglePin={togglePin}
|
|
826
|
+
title="Files"
|
|
827
|
+
/>
|
|
396
828
|
)}
|
|
829
|
+
</div>
|
|
830
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
831
|
+
{openTabs.size > 0 && (
|
|
832
|
+
<div className="border-b bg-muted/10">
|
|
833
|
+
{/* Tab bar */}
|
|
834
|
+
<div className="flex items-center border-b bg-muted/30">
|
|
835
|
+
<div className="flex-1 flex items-center overflow-x-auto min-w-0">
|
|
836
|
+
{[...openTabs.entries()].map(([path]) => {
|
|
837
|
+
const fileName = path.split("/").pop() ?? path;
|
|
838
|
+
const isActive = path === activeTabPath;
|
|
839
|
+
return (
|
|
840
|
+
<button
|
|
841
|
+
key={path}
|
|
842
|
+
onClick={() => {
|
|
843
|
+
setActiveTabPath(path);
|
|
844
|
+
setWorkspaceActivePath(path);
|
|
845
|
+
setPreviewCollapsed(false);
|
|
846
|
+
}}
|
|
847
|
+
className={`group relative flex items-center gap-1.5 px-3 py-1.5 text-xs border-r shrink-0 max-w-[180px] ${
|
|
848
|
+
isActive
|
|
849
|
+
? "bg-background text-foreground border-b-2 border-b-primary"
|
|
850
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
851
|
+
}`}
|
|
852
|
+
title={path}
|
|
853
|
+
>
|
|
854
|
+
<span className="truncate">{fileName}</span>
|
|
855
|
+
<span
|
|
856
|
+
role="button"
|
|
857
|
+
onClick={(e) => {
|
|
858
|
+
e.stopPropagation();
|
|
859
|
+
closeTab(path);
|
|
860
|
+
}}
|
|
861
|
+
className="shrink-0 p-0.5 rounded hover:bg-muted-foreground/20 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
862
|
+
title="Close tab"
|
|
863
|
+
>
|
|
864
|
+
<X className="h-3 w-3" />
|
|
865
|
+
</span>
|
|
866
|
+
</button>
|
|
867
|
+
);
|
|
868
|
+
})}
|
|
869
|
+
</div>
|
|
870
|
+
<div className="flex items-center gap-0.5 px-1 shrink-0">
|
|
871
|
+
<button
|
|
872
|
+
onClick={() => setPreviewCollapsed((p) => !p)}
|
|
873
|
+
className="p-1 rounded hover:bg-muted"
|
|
874
|
+
title={previewCollapsed ? "Expand preview" : "Collapse preview"}
|
|
875
|
+
>
|
|
876
|
+
{previewCollapsed ? (
|
|
877
|
+
<ChevronDown className="h-3.5 w-3.5" />
|
|
878
|
+
) : (
|
|
879
|
+
<Minus className="h-3.5 w-3.5" />
|
|
880
|
+
)}
|
|
881
|
+
</button>
|
|
882
|
+
<button
|
|
883
|
+
onClick={closeAllTabs}
|
|
884
|
+
className="p-1 rounded hover:bg-muted"
|
|
885
|
+
title="Close all tabs"
|
|
886
|
+
>
|
|
887
|
+
<X className="h-3.5 w-3.5" />
|
|
888
|
+
</button>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
397
891
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
892
|
+
{/* Active tab content */}
|
|
893
|
+
{!previewCollapsed && activeTabPath && openTabs.has(activeTabPath) && (() => {
|
|
894
|
+
const tab = openTabs.get(activeTabPath)!;
|
|
895
|
+
return (
|
|
896
|
+
<div key={activeTabPath} className="bg-white min-h-24">
|
|
897
|
+
{tab.loading ? (
|
|
898
|
+
<div className="p-3 flex items-center gap-2 text-muted-foreground">
|
|
899
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
900
|
+
<span className="text-sm">Loading file preview...</span>
|
|
901
|
+
</div>
|
|
902
|
+
) : tab.error ? (
|
|
903
|
+
<div className="p-3 text-sm text-destructive flex items-center gap-2">
|
|
904
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
905
|
+
<span>{tab.error}</span>
|
|
906
|
+
</div>
|
|
907
|
+
) : (
|
|
908
|
+
<CodePreview
|
|
909
|
+
code={tab.code}
|
|
910
|
+
compiler={compiler}
|
|
911
|
+
services={namespaces}
|
|
912
|
+
filePath={activeTabPath}
|
|
913
|
+
onOpenEditSession={openSharedEditSession}
|
|
914
|
+
/>
|
|
915
|
+
)}
|
|
916
|
+
</div>
|
|
917
|
+
);
|
|
918
|
+
})()}
|
|
919
|
+
</div>
|
|
920
|
+
)}
|
|
921
|
+
<ScrollArea className="h-full flex-1" ref={scrollRef}>
|
|
922
|
+
<div className="p-4 space-y-4">
|
|
923
|
+
{messages.length === 0 ? (
|
|
924
|
+
<div className="text-center text-muted-foreground py-12">
|
|
402
925
|
<img
|
|
403
926
|
src={APROVAN_LOGO}
|
|
404
927
|
alt=""
|
|
405
|
-
className="rounded-full"
|
|
928
|
+
className="h-12 w-12 mx-auto mb-4 opacity-50 rounded-full"
|
|
406
929
|
/>
|
|
407
|
-
<
|
|
408
|
-
</Avatar>
|
|
409
|
-
<div className="flex flex-col gap-1">
|
|
410
|
-
<div className="h-5" />
|
|
411
|
-
<div className="bg-muted rounded-lg px-4 py-2">
|
|
412
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
413
|
-
</div>
|
|
930
|
+
<p>Start a conversation</p>
|
|
414
931
|
</div>
|
|
415
|
-
|
|
416
|
-
|
|
932
|
+
) : (
|
|
933
|
+
messages.map((msg) => (
|
|
934
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
935
|
+
))
|
|
936
|
+
)}
|
|
937
|
+
|
|
938
|
+
{isLoading &&
|
|
939
|
+
messages[messages.length - 1]?.role !== "assistant" && (
|
|
940
|
+
<div className="flex gap-3 justify-start">
|
|
941
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
942
|
+
<img
|
|
943
|
+
src={APROVAN_LOGO}
|
|
944
|
+
alt=""
|
|
945
|
+
className="rounded-full"
|
|
946
|
+
/>
|
|
947
|
+
<AvatarFallback>A</AvatarFallback>
|
|
948
|
+
</Avatar>
|
|
949
|
+
<div className="flex flex-col gap-1">
|
|
950
|
+
<div className="h-5" />
|
|
951
|
+
<div className="bg-muted rounded-lg px-4 py-2">
|
|
952
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
)}
|
|
957
|
+
</div>
|
|
958
|
+
</ScrollArea>
|
|
959
|
+
</div>
|
|
960
|
+
</CardContent>
|
|
961
|
+
|
|
962
|
+
{error && (
|
|
963
|
+
<div className="px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2">
|
|
964
|
+
<AlertCircle className="h-4 w-4" />
|
|
965
|
+
{error.message}
|
|
417
966
|
</div>
|
|
418
|
-
|
|
419
|
-
</CardContent>
|
|
967
|
+
)}
|
|
420
968
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
969
|
+
<div className="p-4 border-t">
|
|
970
|
+
<form onSubmit={handleSubmit} className="flex gap-2 items-end">
|
|
971
|
+
<MarkdownEditor
|
|
972
|
+
value={input}
|
|
973
|
+
onChange={setInput}
|
|
974
|
+
onSubmit={() => {
|
|
975
|
+
if (!isLoading && input.trim()) {
|
|
976
|
+
handleSubmit();
|
|
977
|
+
}
|
|
978
|
+
}}
|
|
979
|
+
placeholder="Type a message... (Shift+Enter for new line)"
|
|
980
|
+
disabled={isLoading}
|
|
981
|
+
/>
|
|
982
|
+
<Button
|
|
983
|
+
type="submit"
|
|
984
|
+
disabled={isLoading || !input.trim()}
|
|
985
|
+
className="shrink-0"
|
|
986
|
+
>
|
|
987
|
+
{isLoading ? (
|
|
988
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
989
|
+
) : (
|
|
990
|
+
<Send className="h-4 w-4" />
|
|
991
|
+
)}
|
|
992
|
+
</Button>
|
|
993
|
+
</form>
|
|
425
994
|
</div>
|
|
426
|
-
|
|
995
|
+
</Card>
|
|
427
996
|
|
|
428
|
-
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
997
|
+
{!editSession && (
|
|
998
|
+
<Bobbin
|
|
999
|
+
container={chatContainer}
|
|
1000
|
+
pillContainer={chatContainer}
|
|
1001
|
+
defaultActive={false}
|
|
1002
|
+
showInspector
|
|
1003
|
+
onChanges={() => undefined}
|
|
1004
|
+
exclude={[".bobbin-pill", "[data-bobbin]"]}
|
|
1005
|
+
/>
|
|
1006
|
+
)}
|
|
1007
|
+
</div>
|
|
1008
|
+
{editSession && (
|
|
1009
|
+
<EditModal
|
|
1010
|
+
isOpen
|
|
1011
|
+
onClose={() => setEditSession(null)}
|
|
1012
|
+
onSaveProject={async (project) => {
|
|
1013
|
+
await saveWorkspaceProject(project);
|
|
1014
|
+
await refreshWorkspace();
|
|
1015
|
+
}}
|
|
1016
|
+
originalProject={editSession.project}
|
|
1017
|
+
initialActiveFile={editSession.initialActiveFile}
|
|
1018
|
+
initialTreePath={editSession.initialTreePath}
|
|
1019
|
+
apiEndpoint="/api/edit"
|
|
1020
|
+
initialState={{ showPreview: true, showTree: true }}
|
|
1021
|
+
compile={async (code) => {
|
|
1022
|
+
if (!compiler) return { success: true };
|
|
1023
|
+
try {
|
|
1024
|
+
await compiler.compile(
|
|
1025
|
+
code,
|
|
1026
|
+
createPreviewManifest(namespaces),
|
|
1027
|
+
{ typescript: true },
|
|
1028
|
+
);
|
|
1029
|
+
return { success: true };
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
return {
|
|
1032
|
+
success: false,
|
|
1033
|
+
error:
|
|
1034
|
+
err instanceof Error ? err.message : "Compilation failed",
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
}}
|
|
1038
|
+
renderPreview={(code) => (
|
|
1039
|
+
<WidgetPreview
|
|
1040
|
+
code={code}
|
|
1041
|
+
compiler={compiler}
|
|
1042
|
+
services={namespaces}
|
|
443
1043
|
/>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
{isLoading ? (
|
|
450
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
451
|
-
) : (
|
|
452
|
-
<Send className="h-4 w-4" />
|
|
453
|
-
)}
|
|
454
|
-
</Button>
|
|
455
|
-
</form>
|
|
456
|
-
</div>
|
|
457
|
-
</Card>
|
|
458
|
-
</div>
|
|
1044
|
+
)}
|
|
1045
|
+
previewLoading={!compiler}
|
|
1046
|
+
/>
|
|
1047
|
+
)}
|
|
1048
|
+
</SharedEditSessionCtx.Provider>
|
|
459
1049
|
</PatchworkCtx.Provider>
|
|
460
1050
|
);
|
|
461
1051
|
}
|