@nextclaw/ui 0.6.3 → 0.6.4

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{ChannelsList-Bga6n85j.js → ChannelsList-D_sFrLcv.js} +1 -1
  3. package/dist/assets/ChatPage-dDje_-4b.js +32 -0
  4. package/dist/assets/{DocBrowser-dv57PRp5.js → DocBrowser-BjlljUNM.js} +1 -1
  5. package/dist/assets/{MarketplacePage-j6p73Hjo.js → MarketplacePage-Dob7bQI5.js} +1 -1
  6. package/dist/assets/{ModelConfig-BiKSDp5h.js → ModelConfig-BesYoiw5.js} +1 -1
  7. package/dist/assets/{ProvidersList-B7ZfRUkD.js → ProvidersList-NOoQyb_C.js} +1 -1
  8. package/dist/assets/{RuntimeConfig-Bpt9UNb6.js → RuntimeConfig-CGLGP3_g.js} +1 -1
  9. package/dist/assets/{SecretsConfig-Ds00G-_O.js → SecretsConfig-DP_InKs3.js} +1 -1
  10. package/dist/assets/{SessionsConfig-Mjet4opU.js → SessionsConfig-XJbSPDZp.js} +1 -1
  11. package/dist/assets/{card-C7JJ5BGA.js → card-Bg0moANO.js} +1 -1
  12. package/dist/assets/{index-Cb9xiqC5.js → index-8RAlp6Gn.js} +2 -2
  13. package/dist/assets/index-r3k4oB78.css +1 -0
  14. package/dist/assets/{label-DHJKdaUl.js → label-DZuLJB_b.js} +1 -1
  15. package/dist/assets/{logos-fPO_amyL.js → logos-BDO7tmL-.js} +1 -1
  16. package/dist/assets/{page-layout-CF0JQsWW.js → page-layout-BL29v3Ct.js} +1 -1
  17. package/dist/assets/{switch-C1hgy-fE.js → switch-DxH8lRYW.js} +1 -1
  18. package/dist/assets/{tabs-custom-OyoLf5ZM.js → tabs-custom-BPd_6USz.js} +1 -1
  19. package/dist/assets/{useConfig-D_G46zbo.js → useConfig-CJjG7ys-.js} +1 -1
  20. package/dist/assets/{useConfirmDialog-_0u6i3cI.js → useConfirmDialog-C-G5k7zK.js} +1 -1
  21. package/dist/index.html +2 -2
  22. package/package.json +1 -1
  23. package/src/components/chat/ChatConversationPanel.tsx +33 -0
  24. package/src/components/chat/ChatInputBar.tsx +32 -4
  25. package/src/components/chat/ChatThread.tsx +149 -10
  26. package/dist/assets/ChatPage-B-Yk3kkv.js +0 -32
  27. package/dist/assets/index-BiJ2xs5X.css +0 -1
@@ -1,4 +1,4 @@
1
- import{r as i,ab as b,aT as I,a8 as ie,j as s,aa as j,ag as le,ac as h,ad as D,ah as ce,ai as de,aj as ue,al as fe,am as ge,an as me,ak as pe,aU as ve,as as xe}from"./vendor-Ylg6Wdt_.js";import{c as N,t as x}from"./index-Cb9xiqC5.js";import{B as w}from"./page-layout-CF0JQsWW.js";function Ne(e,t){return i.useReducer((o,a)=>t[o][a]??o,e)}var E=e=>{const{present:t,children:o}=e,a=De(t),r=typeof o=="function"?o({present:a.isPresent}):i.Children.only(o),n=b(a.ref,he(r));return typeof o=="function"||a.isPresent?i.cloneElement(r,{ref:n}):null};E.displayName="Presence";function De(e){const[t,o]=i.useState(),a=i.useRef(null),r=i.useRef(e),n=i.useRef("none"),l=e?"mounted":"unmounted",[c,u]=Ne(l,{mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}});return i.useEffect(()=>{const d=y(a.current);n.current=c==="mounted"?d:"none"},[c]),I(()=>{const d=a.current,f=r.current;if(f!==e){const C=n.current,p=y(d);e?u("MOUNT"):p==="none"||(d==null?void 0:d.display)==="none"?u("UNMOUNT"):u(f&&C!==p?"ANIMATION_OUT":"UNMOUNT"),r.current=e}},[e,u]),I(()=>{if(t){let d;const f=t.ownerDocument.defaultView??window,m=p=>{const re=y(a.current).includes(CSS.escape(p.animationName));if(p.target===t&&re&&(u("ANIMATION_END"),!r.current)){const se=t.style.animationFillMode;t.style.animationFillMode="forwards",d=f.setTimeout(()=>{t.style.animationFillMode==="forwards"&&(t.style.animationFillMode=se)})}},C=p=>{p.target===t&&(n.current=y(a.current))};return t.addEventListener("animationstart",C),t.addEventListener("animationcancel",m),t.addEventListener("animationend",m),()=>{f.clearTimeout(d),t.removeEventListener("animationstart",C),t.removeEventListener("animationcancel",m),t.removeEventListener("animationend",m)}}else u("ANIMATION_END")},[t,u]),{isPresent:["mounted","unmountSuspended"].includes(c),ref:i.useCallback(d=>{a.current=d?getComputedStyle(d):null,o(d)},[])}}function y(e){return(e==null?void 0:e.animationName)||"none"}function he(e){var a,r;let t=(a=Object.getOwnPropertyDescriptor(e.props,"ref"))==null?void 0:a.get,o=t&&"isReactWarning"in t&&t.isReactWarning;return o?e.ref:(t=(r=Object.getOwnPropertyDescriptor(e,"ref"))==null?void 0:r.get,o=t&&"isReactWarning"in t&&t.isReactWarning,o?e.props.ref:e.props.ref||e.ref)}var O="Dialog",[M]=ce(O),[Ce,g]=M(O),T=e=>{const{__scopeDialog:t,children:o,open:a,defaultOpen:r,onOpenChange:n,modal:l=!0}=e,c=i.useRef(null),u=i.useRef(null),[d,f]=ie({prop:a,defaultProp:r??!1,onChange:n,caller:O});return s.jsx(Ce,{scope:t,triggerRef:c,contentRef:u,contentId:j(),titleId:j(),descriptionId:j(),open:d,onOpenChange:f,onOpenToggle:i.useCallback(()=>f(m=>!m),[f]),modal:l,children:o})};T.displayName=O;var S="DialogTrigger",ye=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(S,o),n=b(t,r.triggerRef);return s.jsx(h.button,{type:"button","aria-haspopup":"dialog","aria-expanded":r.open,"aria-controls":r.contentId,"data-state":_(r.open),...a,ref:n,onClick:D(e.onClick,r.onOpenToggle)})});ye.displayName=S;var A="DialogPortal",[Re,F]=M(A,{forceMount:void 0}),L=e=>{const{__scopeDialog:t,forceMount:o,children:a,container:r}=e,n=g(A,t);return s.jsx(Re,{scope:t,forceMount:o,children:i.Children.map(a,l=>s.jsx(E,{present:o||n.open,children:s.jsx(le,{asChild:!0,container:r,children:l})}))})};L.displayName=A;var R="DialogOverlay",k=i.forwardRef((e,t)=>{const o=F(R,e.__scopeDialog),{forceMount:a=o.forceMount,...r}=e,n=g(R,e.__scopeDialog);return n.modal?s.jsx(E,{present:a||n.open,children:s.jsx(Ee,{...r,ref:t})}):null});k.displayName=R;var be=pe("DialogOverlay.RemoveScroll"),Ee=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(R,o);return s.jsx(ue,{as:be,allowPinchZoom:!0,shards:[r.contentRef],children:s.jsx(h.div,{"data-state":_(r.open),...a,ref:t,style:{pointerEvents:"auto",...a.style}})})}),v="DialogContent",W=i.forwardRef((e,t)=>{const o=F(v,e.__scopeDialog),{forceMount:a=o.forceMount,...r}=e,n=g(v,e.__scopeDialog);return s.jsx(E,{present:a||n.open,children:n.modal?s.jsx(Oe,{...r,ref:t}):s.jsx(je,{...r,ref:t})})});W.displayName=v;var Oe=i.forwardRef((e,t)=>{const o=g(v,e.__scopeDialog),a=i.useRef(null),r=b(t,o.contentRef,a);return i.useEffect(()=>{const n=a.current;if(n)return de(n)},[]),s.jsx(U,{...e,ref:r,trapFocus:o.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:D(e.onCloseAutoFocus,n=>{var l;n.preventDefault(),(l=o.triggerRef.current)==null||l.focus()}),onPointerDownOutside:D(e.onPointerDownOutside,n=>{const l=n.detail.originalEvent,c=l.button===0&&l.ctrlKey===!0;(l.button===2||c)&&n.preventDefault()}),onFocusOutside:D(e.onFocusOutside,n=>n.preventDefault())})}),je=i.forwardRef((e,t)=>{const o=g(v,e.__scopeDialog),a=i.useRef(!1),r=i.useRef(!1);return s.jsx(U,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:n=>{var l,c;(l=e.onCloseAutoFocus)==null||l.call(e,n),n.defaultPrevented||(a.current||(c=o.triggerRef.current)==null||c.focus(),n.preventDefault()),a.current=!1,r.current=!1},onInteractOutside:n=>{var u,d;(u=e.onInteractOutside)==null||u.call(e,n),n.defaultPrevented||(a.current=!0,n.detail.originalEvent.type==="pointerdown"&&(r.current=!0));const l=n.target;((d=o.triggerRef.current)==null?void 0:d.contains(l))&&n.preventDefault(),n.detail.originalEvent.type==="focusin"&&r.current&&n.preventDefault()}})}),U=i.forwardRef((e,t)=>{const{__scopeDialog:o,trapFocus:a,onOpenAutoFocus:r,onCloseAutoFocus:n,...l}=e,c=g(v,o),u=i.useRef(null),d=b(t,u);return fe(),s.jsxs(s.Fragment,{children:[s.jsx(ge,{asChild:!0,loop:!0,trapped:a,onMountAutoFocus:r,onUnmountAutoFocus:n,children:s.jsx(me,{role:"dialog",id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":_(c.open),...l,ref:d,onDismiss:()=>c.onOpenChange(!1)})}),s.jsxs(s.Fragment,{children:[s.jsx(Ae,{titleId:c.titleId}),s.jsx(_e,{contentRef:u,descriptionId:c.descriptionId})]})]})}),P="DialogTitle",$=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(P,o);return s.jsx(h.h2,{id:r.titleId,...a,ref:t})});$.displayName=P;var G="DialogDescription",B=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(G,o);return s.jsx(h.p,{id:r.descriptionId,...a,ref:t})});B.displayName=G;var z="DialogClose",H=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(z,o);return s.jsx(h.button,{type:"button",...a,ref:t,onClick:D(e.onClick,()=>r.onOpenChange(!1))})});H.displayName=z;function _(e){return e?"open":"closed"}var V="DialogTitleWarning",[$e,q]=ve(V,{contentName:v,titleName:P,docsSlug:"dialog"}),Ae=({titleId:e})=>{const t=q(V),o=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users.
1
+ import{r as i,ab as b,aT as I,a8 as ie,j as s,aa as j,ag as le,ac as h,ad as D,ah as ce,ai as de,aj as ue,al as fe,am as ge,an as me,ak as pe,aU as ve,as as xe}from"./vendor-Ylg6Wdt_.js";import{c as N,t as x}from"./index-8RAlp6Gn.js";import{B as w}from"./page-layout-BL29v3Ct.js";function Ne(e,t){return i.useReducer((o,a)=>t[o][a]??o,e)}var E=e=>{const{present:t,children:o}=e,a=De(t),r=typeof o=="function"?o({present:a.isPresent}):i.Children.only(o),n=b(a.ref,he(r));return typeof o=="function"||a.isPresent?i.cloneElement(r,{ref:n}):null};E.displayName="Presence";function De(e){const[t,o]=i.useState(),a=i.useRef(null),r=i.useRef(e),n=i.useRef("none"),l=e?"mounted":"unmounted",[c,u]=Ne(l,{mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}});return i.useEffect(()=>{const d=y(a.current);n.current=c==="mounted"?d:"none"},[c]),I(()=>{const d=a.current,f=r.current;if(f!==e){const C=n.current,p=y(d);e?u("MOUNT"):p==="none"||(d==null?void 0:d.display)==="none"?u("UNMOUNT"):u(f&&C!==p?"ANIMATION_OUT":"UNMOUNT"),r.current=e}},[e,u]),I(()=>{if(t){let d;const f=t.ownerDocument.defaultView??window,m=p=>{const re=y(a.current).includes(CSS.escape(p.animationName));if(p.target===t&&re&&(u("ANIMATION_END"),!r.current)){const se=t.style.animationFillMode;t.style.animationFillMode="forwards",d=f.setTimeout(()=>{t.style.animationFillMode==="forwards"&&(t.style.animationFillMode=se)})}},C=p=>{p.target===t&&(n.current=y(a.current))};return t.addEventListener("animationstart",C),t.addEventListener("animationcancel",m),t.addEventListener("animationend",m),()=>{f.clearTimeout(d),t.removeEventListener("animationstart",C),t.removeEventListener("animationcancel",m),t.removeEventListener("animationend",m)}}else u("ANIMATION_END")},[t,u]),{isPresent:["mounted","unmountSuspended"].includes(c),ref:i.useCallback(d=>{a.current=d?getComputedStyle(d):null,o(d)},[])}}function y(e){return(e==null?void 0:e.animationName)||"none"}function he(e){var a,r;let t=(a=Object.getOwnPropertyDescriptor(e.props,"ref"))==null?void 0:a.get,o=t&&"isReactWarning"in t&&t.isReactWarning;return o?e.ref:(t=(r=Object.getOwnPropertyDescriptor(e,"ref"))==null?void 0:r.get,o=t&&"isReactWarning"in t&&t.isReactWarning,o?e.props.ref:e.props.ref||e.ref)}var O="Dialog",[M]=ce(O),[Ce,g]=M(O),T=e=>{const{__scopeDialog:t,children:o,open:a,defaultOpen:r,onOpenChange:n,modal:l=!0}=e,c=i.useRef(null),u=i.useRef(null),[d,f]=ie({prop:a,defaultProp:r??!1,onChange:n,caller:O});return s.jsx(Ce,{scope:t,triggerRef:c,contentRef:u,contentId:j(),titleId:j(),descriptionId:j(),open:d,onOpenChange:f,onOpenToggle:i.useCallback(()=>f(m=>!m),[f]),modal:l,children:o})};T.displayName=O;var S="DialogTrigger",ye=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(S,o),n=b(t,r.triggerRef);return s.jsx(h.button,{type:"button","aria-haspopup":"dialog","aria-expanded":r.open,"aria-controls":r.contentId,"data-state":_(r.open),...a,ref:n,onClick:D(e.onClick,r.onOpenToggle)})});ye.displayName=S;var A="DialogPortal",[Re,F]=M(A,{forceMount:void 0}),L=e=>{const{__scopeDialog:t,forceMount:o,children:a,container:r}=e,n=g(A,t);return s.jsx(Re,{scope:t,forceMount:o,children:i.Children.map(a,l=>s.jsx(E,{present:o||n.open,children:s.jsx(le,{asChild:!0,container:r,children:l})}))})};L.displayName=A;var R="DialogOverlay",k=i.forwardRef((e,t)=>{const o=F(R,e.__scopeDialog),{forceMount:a=o.forceMount,...r}=e,n=g(R,e.__scopeDialog);return n.modal?s.jsx(E,{present:a||n.open,children:s.jsx(Ee,{...r,ref:t})}):null});k.displayName=R;var be=pe("DialogOverlay.RemoveScroll"),Ee=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(R,o);return s.jsx(ue,{as:be,allowPinchZoom:!0,shards:[r.contentRef],children:s.jsx(h.div,{"data-state":_(r.open),...a,ref:t,style:{pointerEvents:"auto",...a.style}})})}),v="DialogContent",W=i.forwardRef((e,t)=>{const o=F(v,e.__scopeDialog),{forceMount:a=o.forceMount,...r}=e,n=g(v,e.__scopeDialog);return s.jsx(E,{present:a||n.open,children:n.modal?s.jsx(Oe,{...r,ref:t}):s.jsx(je,{...r,ref:t})})});W.displayName=v;var Oe=i.forwardRef((e,t)=>{const o=g(v,e.__scopeDialog),a=i.useRef(null),r=b(t,o.contentRef,a);return i.useEffect(()=>{const n=a.current;if(n)return de(n)},[]),s.jsx(U,{...e,ref:r,trapFocus:o.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:D(e.onCloseAutoFocus,n=>{var l;n.preventDefault(),(l=o.triggerRef.current)==null||l.focus()}),onPointerDownOutside:D(e.onPointerDownOutside,n=>{const l=n.detail.originalEvent,c=l.button===0&&l.ctrlKey===!0;(l.button===2||c)&&n.preventDefault()}),onFocusOutside:D(e.onFocusOutside,n=>n.preventDefault())})}),je=i.forwardRef((e,t)=>{const o=g(v,e.__scopeDialog),a=i.useRef(!1),r=i.useRef(!1);return s.jsx(U,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:n=>{var l,c;(l=e.onCloseAutoFocus)==null||l.call(e,n),n.defaultPrevented||(a.current||(c=o.triggerRef.current)==null||c.focus(),n.preventDefault()),a.current=!1,r.current=!1},onInteractOutside:n=>{var u,d;(u=e.onInteractOutside)==null||u.call(e,n),n.defaultPrevented||(a.current=!0,n.detail.originalEvent.type==="pointerdown"&&(r.current=!0));const l=n.target;((d=o.triggerRef.current)==null?void 0:d.contains(l))&&n.preventDefault(),n.detail.originalEvent.type==="focusin"&&r.current&&n.preventDefault()}})}),U=i.forwardRef((e,t)=>{const{__scopeDialog:o,trapFocus:a,onOpenAutoFocus:r,onCloseAutoFocus:n,...l}=e,c=g(v,o),u=i.useRef(null),d=b(t,u);return fe(),s.jsxs(s.Fragment,{children:[s.jsx(ge,{asChild:!0,loop:!0,trapped:a,onMountAutoFocus:r,onUnmountAutoFocus:n,children:s.jsx(me,{role:"dialog",id:c.contentId,"aria-describedby":c.descriptionId,"aria-labelledby":c.titleId,"data-state":_(c.open),...l,ref:d,onDismiss:()=>c.onOpenChange(!1)})}),s.jsxs(s.Fragment,{children:[s.jsx(Ae,{titleId:c.titleId}),s.jsx(_e,{contentRef:u,descriptionId:c.descriptionId})]})]})}),P="DialogTitle",$=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(P,o);return s.jsx(h.h2,{id:r.titleId,...a,ref:t})});$.displayName=P;var G="DialogDescription",B=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(G,o);return s.jsx(h.p,{id:r.descriptionId,...a,ref:t})});B.displayName=G;var z="DialogClose",H=i.forwardRef((e,t)=>{const{__scopeDialog:o,...a}=e,r=g(z,o);return s.jsx(h.button,{type:"button",...a,ref:t,onClick:D(e.onClick,()=>r.onOpenChange(!1))})});H.displayName=z;function _(e){return e?"open":"closed"}var V="DialogTitleWarning",[$e,q]=ve(V,{contentName:v,titleName:P,docsSlug:"dialog"}),Ae=({titleId:e})=>{const t=q(V),o=`\`${t.contentName}\` requires a \`${t.titleName}\` for the component to be accessible for screen reader users.
2
2
 
3
3
  If you want to hide the \`${t.titleName}\`, you can wrap it with our VisuallyHidden component.
4
4
 
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-Cb9xiqC5.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-8RAlp6Gn.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-Ylg6Wdt_.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-BiJ2xs5X.css">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-r3k4oB78.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,34 @@ type ChatConversationPanelProps = {
40
40
  queuedCount: number;
41
41
  };
42
42
 
43
+ function ChatConversationSkeleton() {
44
+ return (
45
+ <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
46
+ <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
47
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
48
+ <div className="space-y-4">
49
+ <div className="h-6 w-48 animate-pulse rounded bg-gray-200" />
50
+ <div className="h-24 w-[78%] animate-pulse rounded-2xl bg-gray-200/80" />
51
+ <div className="h-20 w-[62%] animate-pulse rounded-2xl bg-gray-200/80" />
52
+ <div className="h-28 w-[84%] animate-pulse rounded-2xl bg-gray-200/80" />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div className="border-t border-gray-200/80 bg-white p-4">
57
+ <div className="mx-auto w-full max-w-[min(1120px,100%)]">
58
+ <div className="rounded-2xl border border-gray-200 bg-white shadow-card p-4">
59
+ <div className="h-16 w-full animate-pulse rounded-xl bg-gray-200/80" />
60
+ <div className="mt-3 flex items-center justify-between">
61
+ <div className="h-8 w-36 animate-pulse rounded-lg bg-gray-200/80" />
62
+ <div className="h-8 w-20 animate-pulse rounded-lg bg-gray-200/80" />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </section>
68
+ );
69
+ }
70
+
43
71
  export function ChatConversationPanel({
44
72
  isProviderStateResolved,
45
73
  modelOptions,
@@ -82,6 +110,10 @@ export function ChatConversationPanel({
82
110
  !isAwaitingAssistantOutput &&
83
111
  !streamingAssistantText.trim();
84
112
 
113
+ if (!isProviderStateResolved) {
114
+ return <ChatConversationSkeleton />;
115
+ }
116
+
85
117
  return (
86
118
  <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
87
119
  {/* Minimal top bar - only shown when session is active */}
@@ -134,6 +166,7 @@ export function ChatConversationPanel({
134
166
 
135
167
  {/* Enhanced input bar */}
136
168
  <ChatInputBar
169
+ isProviderStateResolved={isProviderStateResolved}
137
170
  draft={draft}
138
171
  onDraftChange={onDraftChange}
139
172
  onSend={onSend}
@@ -13,6 +13,7 @@ export type ChatModelOption = {
13
13
  };
14
14
 
15
15
  type ChatInputBarProps = {
16
+ isProviderStateResolved: boolean;
16
17
  draft: string;
17
18
  onDraftChange: (value: string) => void;
18
19
  onSend: () => Promise<void> | void;
@@ -33,6 +34,7 @@ type ChatInputBarProps = {
33
34
  };
34
35
 
35
36
  export function ChatInputBar({
37
+ isProviderStateResolved,
36
38
  draft,
37
39
  onDraftChange,
38
40
  onSend,
@@ -52,7 +54,9 @@ export function ChatInputBar({
52
54
  onSelectedSkillsChange
53
55
  }: ChatInputBarProps) {
54
56
  const hasModelOptions = modelOptions.length > 0;
55
- const inputDisabled = !hasModelOptions && !isSending;
57
+ const isModelOptionsLoading = !isProviderStateResolved && !hasModelOptions;
58
+ const isModelOptionsEmpty = isProviderStateResolved && !hasModelOptions;
59
+ const inputDisabled = (isModelOptionsLoading || isModelOptionsEmpty) && !isSending;
56
60
  const selectedModelOption = modelOptions.find((option) => option.value === selectedModel);
57
61
  const resolvedStopHint =
58
62
  stopDisabledReason === '__preparing__'
@@ -86,10 +90,24 @@ export function ChatInputBar({
86
90
  void onSend();
87
91
  }
88
92
  }}
89
- placeholder={hasModelOptions ? t('chatInputPlaceholder') : t('chatModelNoOptions')}
93
+ placeholder={
94
+ isModelOptionsLoading
95
+ ? ''
96
+ : hasModelOptions
97
+ ? t('chatInputPlaceholder')
98
+ : t('chatModelNoOptions')
99
+ }
90
100
  className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-4 py-3 text-gray-800 placeholder:text-gray-400"
91
101
  />
92
- {!hasModelOptions && (
102
+ {isModelOptionsLoading && (
103
+ <div className="px-4 pb-2">
104
+ <div className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2">
105
+ <span className="h-3 w-28 animate-pulse rounded bg-gray-200" />
106
+ <span className="h-3 w-16 animate-pulse rounded bg-gray-200" />
107
+ </div>
108
+ </div>
109
+ )}
110
+ {isModelOptionsEmpty && (
93
111
  <div className="px-4 pb-2">
94
112
  <div className="inline-flex items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs text-amber-800">
95
113
  <span>{t('chatModelNoOptions')}</span>
@@ -147,13 +165,23 @@ export function ChatInputBar({
147
165
  {selectedModelOption.providerLabel}/{selectedModelOption.modelLabel}
148
166
  </span>
149
167
  </div>
168
+ ) : isModelOptionsLoading ? (
169
+ <div className="h-3 w-24 animate-pulse rounded bg-gray-200" />
150
170
  ) : (
151
171
  <SelectValue placeholder={t('chatSelectModel')} />
152
172
  )}
153
173
  </SelectTrigger>
154
174
  <SelectContent className="w-[320px]">
155
175
  {modelOptions.length === 0 && (
156
- <div className="px-3 py-2 text-xs text-gray-500">{t('chatModelNoOptions')}</div>
176
+ isModelOptionsLoading ? (
177
+ <div className="space-y-2 px-3 py-2">
178
+ <div className="h-3 w-36 animate-pulse rounded bg-gray-200" />
179
+ <div className="h-3 w-28 animate-pulse rounded bg-gray-200" />
180
+ <div className="h-3 w-32 animate-pulse rounded bg-gray-200" />
181
+ </div>
182
+ ) : (
183
+ <div className="px-3 py-2 text-xs text-gray-500">{t('chatModelNoOptions')}</div>
184
+ )
157
185
  )}
158
186
  {modelOptions.map((option) => (
159
187
  <SelectItem key={option.value} value={option.value} className="py-2">
@@ -1,4 +1,4 @@
1
- import { useMemo, type ReactNode } from 'react';
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
2
2
  import type { SessionEventView, SessionMessageView } from '@/api/types';
3
3
  import { cn } from '@/lib/utils';
4
4
  import {
@@ -11,10 +11,9 @@ import {
11
11
  type ToolCard
12
12
  } from '@/lib/chat-message';
13
13
  import { formatDateTime, t } from '@/lib/i18n';
14
- import ReactMarkdown from 'react-markdown';
15
- import rehypeSanitize from 'rehype-sanitize';
14
+ import ReactMarkdown, { type Components } from 'react-markdown';
16
15
  import remarkGfm from 'remark-gfm';
17
- import { Bot, Clock3, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
16
+ import { Bot, Check, Clock3, Copy, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
18
17
 
19
18
  type ChatThreadProps = {
20
19
  events: SessionEventView[];
@@ -24,6 +23,8 @@ type ChatThreadProps = {
24
23
 
25
24
  const MARKDOWN_MAX_CHARS = 140_000;
26
25
  const TOOL_OUTPUT_PREVIEW_MAX = 220;
26
+ const CODE_LANGUAGE_REGEX = /language-([a-z0-9-]+)/i;
27
+ const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
27
28
 
28
29
  type WorkflowToolCard = ToolCard & {
29
30
  _workflowStep?: number;
@@ -36,6 +37,91 @@ function trimMarkdown(value: string): string {
36
37
  return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n…`;
37
38
  }
38
39
 
40
+ function flattenNodeText(value: ReactNode): string {
41
+ if (typeof value === 'string' || typeof value === 'number') {
42
+ return String(value);
43
+ }
44
+ if (Array.isArray(value)) {
45
+ return value.map(flattenNodeText).join('');
46
+ }
47
+ return '';
48
+ }
49
+
50
+ function normalizeCodeText(value: ReactNode): string {
51
+ const content = flattenNodeText(value);
52
+ return content.endsWith('\n') ? content.slice(0, -1) : content;
53
+ }
54
+
55
+ function resolveCodeLanguage(className?: string): string {
56
+ const match = className ? CODE_LANGUAGE_REGEX.exec(className) : null;
57
+ return match?.[1]?.toLowerCase() || 'text';
58
+ }
59
+
60
+ function resolveSafeHref(href?: string): string | null {
61
+ if (!href) {
62
+ return null;
63
+ }
64
+ if (href.startsWith('#') || href.startsWith('/') || href.startsWith('./') || href.startsWith('../')) {
65
+ return href;
66
+ }
67
+ try {
68
+ const url = new URL(href);
69
+ return SAFE_LINK_PROTOCOLS.has(url.protocol) ? href : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function isExternalHref(href: string): boolean {
76
+ return /^https?:\/\//i.test(href);
77
+ }
78
+
79
+ function MarkdownCodeBlock({ className, children }: { className?: string; children: ReactNode }) {
80
+ const [copied, setCopied] = useState(false);
81
+ const language = useMemo(() => resolveCodeLanguage(className), [className]);
82
+ const codeText = useMemo(() => normalizeCodeText(children), [children]);
83
+
84
+ const handleCopy = useCallback(async () => {
85
+ if (!codeText || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
86
+ return;
87
+ }
88
+ try {
89
+ await navigator.clipboard.writeText(codeText);
90
+ setCopied(true);
91
+ } catch {
92
+ setCopied(false);
93
+ }
94
+ }, [codeText]);
95
+
96
+ useEffect(() => {
97
+ if (!copied || typeof window === 'undefined') {
98
+ return;
99
+ }
100
+ const timer = window.setTimeout(() => setCopied(false), 1300);
101
+ return () => window.clearTimeout(timer);
102
+ }, [copied]);
103
+
104
+ return (
105
+ <div className="chat-codeblock">
106
+ <div className="chat-codeblock-toolbar">
107
+ <span className="chat-codeblock-language">{language}</span>
108
+ <button
109
+ type="button"
110
+ className="chat-codeblock-copy"
111
+ onClick={handleCopy}
112
+ aria-label={copied ? t('chatCodeCopied') : t('chatCodeCopy')}
113
+ >
114
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
115
+ <span>{copied ? t('chatCodeCopied') : t('chatCodeCopy')}</span>
116
+ </button>
117
+ </div>
118
+ <pre>
119
+ <code className={className}>{codeText}</code>
120
+ </pre>
121
+ </div>
122
+ );
123
+ }
124
+
39
125
  function roleTitle(role: ChatRole): string {
40
126
  if (role === 'user') return t('chatRoleUser');
41
127
  if (role === 'assistant') return t('chatRoleAssistant');
@@ -91,16 +177,69 @@ function RoleAvatar({ role }: { role: ChatRole }) {
91
177
 
92
178
  function MarkdownBlock({ text, role }: { text: string; role: ChatRole }) {
93
179
  const isUser = role === 'user';
180
+ const markdownComponents = useMemo<Components>(() => ({
181
+ a: ({ href, children, ...props }) => {
182
+ const safeHref = resolveSafeHref(href);
183
+ if (!safeHref) {
184
+ return <span className="chat-link-invalid">{children}</span>;
185
+ }
186
+ const external = isExternalHref(safeHref);
187
+ return (
188
+ <a
189
+ {...props}
190
+ href={safeHref}
191
+ target={external ? '_blank' : undefined}
192
+ rel={external ? 'noreferrer noopener' : undefined}
193
+ >
194
+ {children}
195
+ </a>
196
+ );
197
+ },
198
+ table: ({ children, ...props }) => (
199
+ <div className="chat-table-wrap">
200
+ <table {...props}>{children}</table>
201
+ </div>
202
+ ),
203
+ input: ({ type, checked, ...props }) => {
204
+ if (type !== 'checkbox') {
205
+ return <input {...props} type={type} />;
206
+ }
207
+ return (
208
+ <input
209
+ {...props}
210
+ type="checkbox"
211
+ checked={checked}
212
+ readOnly
213
+ disabled
214
+ className="chat-task-checkbox"
215
+ />
216
+ );
217
+ },
218
+ img: ({ src, alt, ...props }) => {
219
+ const safeSrc = resolveSafeHref(src);
220
+ if (!safeSrc) {
221
+ return null;
222
+ }
223
+ return <img {...props} src={safeSrc} alt={alt || ''} loading="lazy" decoding="async" />;
224
+ },
225
+ code: ({ inline, className, children, ...props }) => {
226
+ if (inline) {
227
+ return (
228
+ <code {...props} className={cn('chat-inline-code', className)}>
229
+ {children}
230
+ </code>
231
+ );
232
+ }
233
+ return <MarkdownCodeBlock className={className}>{children}</MarkdownCodeBlock>;
234
+ }
235
+ }), []);
236
+
94
237
  return (
95
238
  <div className={cn('chat-markdown', isUser ? 'chat-markdown-user' : 'chat-markdown-assistant')}>
96
239
  <ReactMarkdown
240
+ skipHtml
97
241
  remarkPlugins={[remarkGfm]}
98
- rehypePlugins={[rehypeSanitize]}
99
- components={{
100
- a: ({ ...props }) => (
101
- <a {...props} target="_blank" rel="noreferrer noopener" />
102
- )
103
- }}
242
+ components={markdownComponents}
104
243
  >
105
244
  {trimMarkdown(text)}
106
245
  </ReactMarkdown>