@frankieai/react 0.1.1 → 0.1.5

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/dist/frankie.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid}}}@layer theme{:root,:host{--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-tight:1.25;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.frankie-root{--frankie-background:#fff;--frankie-foreground:#0a0a0a;--frankie-primary:#171717;--frankie-primary-foreground:#fafafa;--frankie-secondary:#f5f5f5;--frankie-secondary-foreground:#171717;--frankie-muted:#f5f5f5;--frankie-muted-foreground:#737373;--frankie-border:#e5e5e5;--frankie-radius:.625rem;--frankie-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;font-family:var(--frankie-font);color:var(--frankie-foreground);-webkit-font-smoothing:antialiased;line-height:1.5}.frankie-root.dark{--frankie-background:#0a0a0a;--frankie-foreground:#fafafa;--frankie-primary:#fafafa;--frankie-primary-foreground:#171717;--frankie-secondary:#262626;--frankie-secondary-foreground:#fafafa;--frankie-muted:#262626;--frankie-muted-foreground:#a3a3a3;--frankie-border:#262626}.frankie-root,.frankie-root *,.frankie-root :before,.frankie-root :after{box-sizing:border-box}.frankie-root button,.frankie-root textarea,.frankie-root input{font:inherit;color:inherit;margin:0}.frankie-root textarea{background-color:var(--frankie-background)}.frankie-focus:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:2px}.frankie-scroll{scrollbar-width:thin;scrollbar-color:var(--frankie-border) transparent}.frankie-prose>*+*{margin-top:.5rem}.frankie-prose p{white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.frankie-prose ul,.frankie-prose ol{flex-direction:column;gap:.25rem;margin:0;padding-left:1.15rem;display:flex}.frankie-prose ul{list-style:outside}.frankie-prose ol{list-style:decimal}.frankie-prose a{color:var(--frankie-primary);text-underline-offset:2px;text-decoration:underline}.frankie-prose code{background-color:var(--frankie-muted);border-radius:.25rem;padding:.1em .3em;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.85em}.frankie-prose pre{border-radius:var(--frankie-radius);background-color:var(--frankie-muted);margin:0;padding:.625rem .75rem;overflow-x:auto}.frankie-prose pre code{background:0 0;padding:0}}@layer utilities{.fixed{position:fixed}.isolate{isolation:isolate}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-full{width:100%;height:100%}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-\[32rem\]{height:32rem}.h-full{height:100%}.h-px{height:1px}.min-h-\[2\.25rem\]{min-height:2.25rem}.w-\[22rem\]{width:22rem}.w-full{width:100%}.max-w-\[8rem\]{max-width:8rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[14rem\]{max-width:14rem}.max-w-\[85\%\]{max-width:85%}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.resize-none{resize:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded-\[calc\(var\(--frankie-radius\)\+4px\)\]{border-radius:calc(var(--frankie-radius) + 4px)}.rounded-\[calc\(var\(--frankie-radius\)-2px\)\]{border-radius:calc(var(--frankie-radius) - 2px)}.rounded-\[var\(--frankie-radius\)\]{border-radius:var(--frankie-radius)}.rounded-full{border-radius:3.40282e38px}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-border{border-color:var(--frankie-border)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--frankie-background)}.bg-border{background-color:var(--frankie-border)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-muted{background-color:var(--frankie-muted)}.bg-muted-foreground{background-color:var(--frankie-muted-foreground)}.bg-primary{background-color:var(--frankie-primary)}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-secondary{background-color:var(--frankie-secondary)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.text-\[0\.6875rem\]{font-size:.6875rem}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-emerald-600{color:var(--color-emerald-600)}.text-foreground{color:var(--frankie-foreground)}.text-muted-foreground{color:var(--frankie-muted-foreground)}.text-primary{color:var(--frankie-primary)}.text-primary-foreground{color:var(--frankie-primary-foreground)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--frankie-secondary-foreground)}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.placeholder\:text-muted-foreground::placeholder{color:var(--frankie-muted-foreground)}@media (hover:hover){.hover\:bg-muted:hover{background-color:var(--frankie-muted)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.dark\:text-emerald-400:where(.dark,.dark *){color:var(--color-emerald-400)}.dark\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid}}}@layer theme{:root,:host{--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-tight:1.25;--radius-md:.375rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.frankie-root{--frankie-background:#fff;--frankie-foreground:#0a0a0a;--frankie-primary:#171717;--frankie-primary-foreground:#fafafa;--frankie-secondary:#f5f5f5;--frankie-secondary-foreground:#171717;--frankie-muted:#f5f5f5;--frankie-muted-foreground:#737373;--frankie-border:#e5e5e5;--frankie-radius:.625rem;--frankie-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;font-family:var(--frankie-font);color:var(--frankie-foreground);-webkit-font-smoothing:antialiased;line-height:1.5}.frankie-root.dark{--frankie-background:#0a0a0a;--frankie-foreground:#fafafa;--frankie-primary:#fafafa;--frankie-primary-foreground:#171717;--frankie-secondary:#262626;--frankie-secondary-foreground:#fafafa;--frankie-muted:#262626;--frankie-muted-foreground:#a3a3a3;--frankie-border:#262626}.frankie-root,.frankie-root *,.frankie-root :before,.frankie-root :after{box-sizing:border-box}.frankie-root button,.frankie-root textarea,.frankie-root input{font:inherit;color:inherit;margin:0}.frankie-root textarea{background-color:var(--frankie-background)}.frankie-focus:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:2px}.frankie-scroll{scrollbar-width:thin;scrollbar-color:var(--frankie-border) transparent}.frankie-prose>*+*{margin-top:.5rem}.frankie-prose p{white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.frankie-prose ul,.frankie-prose ol{flex-direction:column;gap:.25rem;margin:0;padding-left:1.15rem;display:flex}.frankie-prose ul{list-style:outside}.frankie-prose ol{list-style:decimal}.frankie-prose a{color:var(--frankie-primary);text-underline-offset:2px;text-decoration:underline}.frankie-prose code{background-color:var(--frankie-muted);border-radius:.25rem;padding:.1em .3em;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.85em}.frankie-prose pre{border-radius:var(--frankie-radius);background-color:var(--frankie-muted);margin:0;padding:.625rem .75rem;overflow-x:auto}.frankie-prose pre code{background:0 0;padding:0}.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, var(--frankie-primary), transparent 65%), var(--frankie-background);flex-direction:column;min-width:0;display:flex;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, color-mix(in srgb, var(--frankie-primary) 8%, transparent), transparent 65%), var(--frankie-background)}}.frankie-chat-page{color:var(--frankie-foreground)}.frankie-page-header{border-bottom:1px solid var(--frankie-border);background:var(--frankie-background);flex:0 0 56px;justify-content:space-between;align-items:center;gap:16px;min-height:56px;padding:0 24px;display:flex}@supports (color:color-mix(in lab, red, red)){.frankie-page-header{background:color-mix(in srgb, var(--frankie-background) 92%, transparent)}}.frankie-page-header{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.frankie-page-header>div{align-items:center;gap:10px;display:flex}.frankie-page-header strong{font-size:.925rem;font-weight:650}.frankie-page-status{background:var(--frankie-primary);width:10px;height:10px;box-shadow:0 0 0 4px var(--frankie-primary);border-radius:999px}@supports (color:color-mix(in lab, red, red)){.frankie-page-status{box-shadow:0 0 0 4px color-mix(in srgb, var(--frankie-primary) 16%, transparent)}}.frankie-page-header button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * .9);background:var(--frankie-secondary);cursor:pointer;min-height:36px;padding:0 13px}.frankie-page-header button:hover{border-color:var(--frankie-primary)}.frankie-page-canvas{flex:1;min-width:0;min-height:0;display:flex}.frankie-page-canvas>.frankie-inline{width:100%;max-width:56rem;margin:0 auto}.frankie-page-canvas .frankie-scroll{padding:2rem clamp(1rem,4vw,4rem) 1.25rem}.frankie-page-canvas .frankie-root>.border-t{background:linear-gradient(180deg, transparent, var(--frankie-background) 30%);border-top:0;padding:.75rem clamp(1rem,4vw,4rem) 1.5rem}.frankie-page-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:1rem;width:min(100%,38rem);min-height:100%;margin:auto;padding:clamp(2rem,7vh,4.5rem) 1rem 2rem;display:flex}.frankie-page-avatar{border-radius:calc(var(--frankie-radius) * 1.4);background:var(--frankie-primary);width:48px;height:48px;color:var(--frankie-primary-foreground);place-items:center;font-weight:750;display:grid}.frankie-page-kicker{color:var(--frankie-primary);letter-spacing:.1em;text-transform:uppercase;margin:0 0 .4rem;font-size:.7rem;font-weight:750}.frankie-page-empty h1{letter-spacing:-.045em;margin:0;font-size:clamp(1.65rem,4vw,2.15rem);line-height:1.08}.frankie-page-empty h1+p{max-width:30rem;color:var(--frankie-muted-foreground);margin:.6rem auto 0}.frankie-page-prompts{gap:.55rem;width:100%;margin-top:.4rem;display:grid}.frankie-page-prompts button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * 1.25);background:var(--frankie-secondary);text-align:left;cursor:pointer;grid-template-columns:2rem minmax(0,1fr) auto;align-items:center;gap:.65rem;min-height:60px;padding:.75rem .9rem;display:grid}.frankie-page-prompts button:hover{border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-page-prompts button:hover{border-color:color-mix(in srgb, var(--frankie-primary) 58%, var(--frankie-border))}}.frankie-page-prompts button:hover{transform:translateY(-1px)}.frankie-page-prompts button>span{color:var(--frankie-muted-foreground);font-size:.68rem;font-weight:700}.frankie-page-prompts button>strong{font-size:.875rem}.frankie-page-prompts button>b{color:var(--frankie-primary);font-size:1rem}@media (max-width:640px){.frankie-page-header{flex-basis:52px;min-height:52px;padding:0 12px}.frankie-page-canvas .frankie-scroll{padding:1.25rem .75rem .5rem}.frankie-page-canvas .frankie-root>.border-t{padding:.65rem .75rem calc(.65rem + env(safe-area-inset-bottom))}}}@layer utilities;@scope(.frankie-root){.visible{visibility:visible}.fixed{position:fixed}.isolate{isolation:isolate}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-full{width:100%;height:100%}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-\[32rem\]{height:32rem}.h-full{height:100%}.h-px{height:1px}.min-h-7{min-height:calc(var(--spacing) * 7)}.min-h-\[2\.25rem\]{min-height:2.25rem}.w-\[22rem\]{width:22rem}.w-full{width:100%}.max-w-\[8rem\]{max-width:8rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[14rem\]{max-width:14rem}.max-w-\[85\%\]{max-width:85%}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded-\[calc\(var\(--frankie-radius\)\+4px\)\]{border-radius:calc(var(--frankie-radius) + 4px)}.rounded-\[calc\(var\(--frankie-radius\)-2px\)\]{border-radius:calc(var(--frankie-radius) - 2px)}.rounded-\[var\(--frankie-radius\)\]{border-radius:var(--frankie-radius)}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--frankie-border)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--frankie-background)}.bg-border{background-color:var(--frankie-border)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-muted{background-color:var(--frankie-muted)}.bg-muted-foreground{background-color:var(--frankie-muted-foreground)}.bg-primary{background-color:var(--frankie-primary)}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-secondary{background-color:var(--frankie-secondary)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.text-\[0\.6875rem\]{font-size:.6875rem}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-emerald-600{color:var(--color-emerald-600)}.text-foreground{color:var(--frankie-foreground)}.text-muted-foreground{color:var(--frankie-muted-foreground)}.text-primary{color:var(--frankie-primary)}.text-primary-foreground{color:var(--frankie-primary-foreground)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--frankie-secondary-foreground)}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.placeholder\:text-muted-foreground::placeholder{color:var(--frankie-muted-foreground)}@media (hover:hover){.hover\:border-border:hover{border-color:var(--frankie-border)}.hover\:bg-muted:hover{background-color:var(--frankie-muted)}.hover\:text-foreground:hover{color:var(--frankie-foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-primary:focus-visible{--tw-ring-color:var(--frankie-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.dark\:text-emerald-400:where(.dark,.dark *){color:var(--color-emerald-400)}.dark\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  Composer: () => Composer,
35
35
  FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
36
36
  FrankieApiError: () => FrankieApiError,
37
+ FrankieChatPage: () => FrankieChatPage,
37
38
  FrankieClient: () => FrankieClient,
38
39
  FrankieInline: () => FrankieInline,
39
40
  FrankieProvider: () => FrankieProvider,
@@ -89,13 +90,16 @@ var FrankieClient = class {
89
90
  get sessionId() {
90
91
  return this.session?.sessionId;
91
92
  }
93
+ get conversationId() {
94
+ return this.options.conversationId;
95
+ }
92
96
  /** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
93
97
  get persist() {
94
98
  return this.options.persist !== false && typeof window !== "undefined";
95
99
  }
96
- /** localStorage namespace for this tenant, shared with the chat hook. */
100
+ /** localStorage namespace for this tenant/conversation, shared with the chat hook. */
97
101
  get namespace() {
98
- return `frankie.${this.options.publishableKey}`;
102
+ return this.options.storageNamespace ?? `frankie.${this.options.publishableKey}`;
99
103
  }
100
104
  readStored() {
101
105
  if (!this.persist) return void 0;
@@ -123,30 +127,80 @@ var FrankieClient = class {
123
127
  });
124
128
  return await this.unwrap(response);
125
129
  }
126
- /** Opens (or reuses) a chat session. */
127
130
  async ensureSession(context) {
128
- if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) {
129
- return this.session;
130
- }
131
+ if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
131
132
  const userToken = await this.options.getUserToken?.();
133
+ if (this.options.getUserToken && !userToken) {
134
+ throw new FrankieApiError(
135
+ "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
136
+ 401,
137
+ "user_token_unavailable"
138
+ );
139
+ }
132
140
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
133
141
  method: "POST",
134
142
  headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
135
- body: JSON.stringify({ ...userToken ? { userToken } : {}, ...context ? { context } : {} })
143
+ body: JSON.stringify({
144
+ ...userToken ? { userToken } : {},
145
+ ...context ? { context } : {},
146
+ ...this.options.conversationId ? { conversationId: this.options.conversationId } : {}
147
+ })
136
148
  });
137
149
  this.session = await this.unwrap(response);
138
150
  this.writeStored();
139
151
  return this.session;
140
152
  }
141
- /** Forget the current session so the next send starts a fresh conversation. */
153
+ async listConversations() {
154
+ const userToken = await this.requireUserToken();
155
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations`, {
156
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
157
+ });
158
+ const body = await this.unwrap(response);
159
+ return Array.isArray(body.conversations) ? body.conversations : [];
160
+ }
161
+ async resumeConversation(id = this.options.conversationId) {
162
+ if (!id) return void 0;
163
+ const userToken = await this.requireUserToken();
164
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
165
+ method: "POST",
166
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
167
+ });
168
+ if (response.status === 404) return void 0;
169
+ const body = await this.unwrap(response);
170
+ this.session = body.session;
171
+ this.writeStored();
172
+ return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
173
+ }
174
+ async syncConversation(messages, title) {
175
+ if (!this.session || !this.options.conversationId) return;
176
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(this.options.conversationId)}`, {
177
+ method: "PUT",
178
+ headers: {
179
+ "content-type": "application/json",
180
+ "x-frankie-key": this.options.publishableKey,
181
+ authorization: `Bearer ${this.session.sessionToken}`
182
+ },
183
+ body: JSON.stringify({ messages, ...title ? { title } : {} })
184
+ });
185
+ await this.unwrap(response);
186
+ }
187
+ async deleteConversation(id) {
188
+ const userToken = await this.requireUserToken();
189
+ const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}`, {
190
+ method: "DELETE",
191
+ headers: { "x-frankie-key": this.options.publishableKey, authorization: `Bearer ${userToken}` }
192
+ });
193
+ if (response.status !== 404) await this.unwrap(response);
194
+ }
195
+ async requireUserToken() {
196
+ const token = await this.options.getUserToken?.();
197
+ if (!token) throw new FrankieApiError("Sign in to access your chat history.", 401, "unauthorized");
198
+ return token;
199
+ }
142
200
  resetSession() {
143
201
  this.session = void 0;
144
202
  this.writeStored();
145
203
  }
146
- /**
147
- * Sends a message and invokes `onEvent` for each server event as it arrives.
148
- * Resolves when the turn ends.
149
- */
150
204
  async send(input, onEvent) {
151
205
  const session = await this.ensureSession(input.context);
152
206
  const body = {
@@ -232,9 +286,8 @@ function FrankieProvider({
232
286
  }) {
233
287
  const client = (0, import_react.useMemo)(
234
288
  () => new FrankieClient(clientOptions),
235
- // The client holds session state; rebuild it only when the endpoint changes.
236
289
  // eslint-disable-next-line react-hooks/exhaustive-deps
237
- [clientOptions.baseUrl, clientOptions.publishableKey]
290
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId]
238
291
  );
239
292
  const [config, setConfig] = (0, import_react.useState)(providedConfig);
240
293
  const [configError, setConfigError] = (0, import_react.useState)(void 0);
@@ -360,8 +413,10 @@ function useFrankieChat() {
360
413
  const [messages, setMessages] = (0, import_react2.useState)([]);
361
414
  const [busy, setBusy] = (0, import_react2.useState)(false);
362
415
  const [error, setError] = (0, import_react2.useState)(void 0);
416
+ const [restored, setRestored] = (0, import_react2.useState)(false);
363
417
  const abortRef = (0, import_react2.useRef)(null);
364
418
  const mounted = (0, import_react2.useRef)(true);
419
+ const messagesRef = (0, import_react2.useRef)([]);
365
420
  (0, import_react2.useEffect)(() => {
366
421
  mounted.current = true;
367
422
  return () => {
@@ -371,28 +426,51 @@ function useFrankieChat() {
371
426
  }, []);
372
427
  const storageKey = `${client.namespace}.messages`;
373
428
  (0, import_react2.useEffect)(() => {
374
- if (!client.persist) return;
375
- try {
376
- const raw = window.localStorage.getItem(storageKey);
377
- const stored = raw ? JSON.parse(raw) : [];
378
- if (Array.isArray(stored) && stored.length) setMessages(stored);
379
- } catch {
380
- }
381
- }, [storageKey]);
429
+ let active = true;
430
+ const restore = async () => {
431
+ if (client.persist) {
432
+ try {
433
+ const raw = window.localStorage.getItem(storageKey);
434
+ const stored = raw ? JSON.parse(raw) : [];
435
+ if (Array.isArray(stored) && stored.length && active) {
436
+ messagesRef.current = stored;
437
+ setMessages(stored);
438
+ }
439
+ } catch {
440
+ }
441
+ }
442
+ try {
443
+ const remote = await client.resumeConversation();
444
+ if (active && remote?.messages.length) {
445
+ const restoredMessages = remote.messages;
446
+ messagesRef.current = restoredMessages;
447
+ setMessages(restoredMessages);
448
+ }
449
+ } catch {
450
+ } finally {
451
+ if (active) setRestored(true);
452
+ }
453
+ };
454
+ void restore();
455
+ return () => {
456
+ active = false;
457
+ };
458
+ }, [client, storageKey]);
382
459
  (0, import_react2.useEffect)(() => {
383
- if (!client.persist || busy) return;
460
+ if (!client.persist || !restored || busy) return;
384
461
  try {
385
462
  if (messages.length) window.localStorage.setItem(storageKey, JSON.stringify(messages));
386
463
  else window.localStorage.removeItem(storageKey);
387
464
  } catch {
388
465
  }
389
- }, [messages, busy, client, storageKey]);
466
+ if (messages.length) void client.syncConversation(messages, conversationTitle(messages)).catch(() => void 0);
467
+ }, [messages, busy, client, storageKey, restored]);
390
468
  const patchAssistant = (0, import_react2.useCallback)((id, update) => {
391
- setMessages(
392
- (current) => current.map(
393
- (message) => message.id === id && message.role === "assistant" ? update(message) : message
394
- )
469
+ const next = messagesRef.current.map(
470
+ (message) => message.id === id && message.role === "assistant" ? update(message) : message
395
471
  );
472
+ messagesRef.current = next;
473
+ setMessages(next);
396
474
  }, []);
397
475
  const send = (0, import_react2.useCallback)(
398
476
  async (text, opts) => {
@@ -403,18 +481,22 @@ function useFrankieChat() {
403
481
  const assistantId = nextId("a");
404
482
  setError(void 0);
405
483
  setBusy(true);
406
- setMessages((current) => [
407
- ...current,
484
+ const next = [
485
+ ...messagesRef.current,
408
486
  newUserMessage(
409
487
  nextId("u"),
410
488
  trimmed,
411
489
  attachments.map((file) => ({ name: file.name, mimeType: file.mimeType }))
412
490
  ),
413
491
  newAssistantMessage(assistantId)
414
- ]);
492
+ ];
493
+ messagesRef.current = next;
494
+ setMessages(next);
415
495
  const abort = new AbortController();
416
496
  abortRef.current = abort;
417
497
  try {
498
+ let receivedText = false;
499
+ let receivedToolActivity = false;
418
500
  await client.send(
419
501
  {
420
502
  message: outgoing,
@@ -425,9 +507,18 @@ function useFrankieChat() {
425
507
  },
426
508
  (event) => {
427
509
  if (event.type === "ping" || event.type === "session") return;
510
+ if (event.type === "text_delta") receivedText = true;
511
+ if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
428
512
  patchAssistant(assistantId, (message) => applyEvent(message, event));
429
513
  }
430
514
  );
515
+ if (!receivedText) {
516
+ patchAssistant(assistantId, (message) => ({
517
+ ...message,
518
+ status: "done",
519
+ text: receivedToolActivity ? "Completed. Open Agent activity for the details." : "Frankie didn\u2019t return an answer. Please try again."
520
+ }));
521
+ }
431
522
  } catch (caught) {
432
523
  if (abort.signal.aborted) {
433
524
  patchAssistant(assistantId, (message) => ({
@@ -436,18 +527,20 @@ function useFrankieChat() {
436
527
  text: message.text || "Stopped."
437
528
  }));
438
529
  } else {
439
- const message = caught instanceof FrankieApiError ? caught.message : "Could not reach the assistant. Check your connection and try again.";
530
+ const message = caught instanceof FrankieApiError ? caught.message : caught instanceof Error && caught.message.startsWith("Frankie") ? caught.message : "Could not reach the assistant. Check your connection and try again.";
440
531
  if (mounted.current) setError(message);
441
532
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
442
533
  }
443
534
  } finally {
444
- if (mounted.current) {
445
- setBusy(false);
446
- patchAssistant(
447
- assistantId,
448
- (message) => message.status === "streaming" ? { ...message, status: "done" } : message
449
- );
535
+ patchAssistant(
536
+ assistantId,
537
+ (message) => message.status === "streaming" ? { ...message, status: "done" } : message
538
+ );
539
+ const transcript = messagesRef.current;
540
+ if (transcript.length) {
541
+ await client.syncConversation(transcript, conversationTitle(transcript)).catch(() => void 0);
450
542
  }
543
+ if (mounted.current) setBusy(false);
451
544
  abortRef.current = null;
452
545
  }
453
546
  },
@@ -457,9 +550,11 @@ function useFrankieChat() {
457
550
  const reset = (0, import_react2.useCallback)(() => {
458
551
  abortRef.current?.abort();
459
552
  client.resetSession();
553
+ messagesRef.current = [];
460
554
  setMessages([]);
461
555
  setError(void 0);
462
556
  setBusy(false);
557
+ setRestored(true);
463
558
  try {
464
559
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
465
560
  } catch {
@@ -467,10 +562,16 @@ function useFrankieChat() {
467
562
  }, [client]);
468
563
  return { messages, busy, ...error ? { error } : {}, send, stop, reset };
469
564
  }
565
+ function conversationTitle(messages) {
566
+ const first = messages.find((message) => message.role === "user" && message.text.trim());
567
+ if (!first) return void 0;
568
+ const title = first.text.trim().replace(/\s+/g, " ");
569
+ return title.length > 42 ? `${title.slice(0, 42)}\u2026` : title;
570
+ }
470
571
 
471
572
  // src/components/chat/FrankieWidget.tsx
472
573
  var import_lucide_react5 = require("lucide-react");
473
- var import_react8 = require("react");
574
+ var import_react9 = require("react");
474
575
 
475
576
  // src/lib/theme.ts
476
577
  function themeToCssVars(theme) {
@@ -531,7 +632,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
531
632
 
532
633
  // src/components/chat/ChatPanel.tsx
533
634
  var import_lucide_react4 = require("lucide-react");
534
- var import_react7 = require("react");
635
+ var import_react8 = require("react");
535
636
 
536
637
  // src/components/ui/avatar.tsx
537
638
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -793,6 +894,7 @@ function readAsBase64(file) {
793
894
 
794
895
  // src/components/chat/MessageBubble.tsx
795
896
  var import_lucide_react3 = require("lucide-react");
897
+ var import_react7 = require("react");
796
898
 
797
899
  // src/components/chat/Markdown.tsx
798
900
  var import_react6 = require("react");
@@ -928,7 +1030,20 @@ function Badge({
928
1030
  var import_jsx_runtime10 = require("react/jsx-runtime");
929
1031
  function ToolActivityList({ tools, renderTool }) {
930
1032
  if (tools.length === 0) return null;
931
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "mt-2 flex flex-col gap-1", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolActivityRow, { tool }) }, tool.id)) });
1033
+ const running = tools.some((tool) => tool.status === "running");
1034
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1035
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1036
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react2.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: running ? "Working" : "Activity" }),
1038
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-[11px]", children: [
1039
+ "\xB7 ",
1040
+ tools.length,
1041
+ " tool call",
1042
+ tools.length === 1 ? "" : "s"
1043
+ ] })
1044
+ ] }),
1045
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1046
+ ] });
932
1047
  }
933
1048
  function ToolActivityRow({ tool }) {
934
1049
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
@@ -977,18 +1092,36 @@ function MessageBubble({
977
1092
  },
978
1093
  `${file.name}-${index}`
979
1094
  )) }) : null,
980
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "max-w-[85%] rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }) : null
1095
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1096
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1097
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text })
1098
+ ] }) : null
981
1099
  ] });
982
1100
  }
983
1101
  const empty = message.text.length === 0 && message.tools.length === 0;
984
1102
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
985
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName, className: "mt-0.5" }),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1104
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1105
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageCopyButton, { text: message.text }) : null
1106
+ ] }),
986
1107
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "min-w-0 max-w-[85%]", children: [
987
1108
  message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text, className: "text-sm" }),
988
1109
  showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
989
1110
  ] })
990
1111
  ] });
991
1112
  }
1113
+ function MessageCopyButton({ text }) {
1114
+ const [copied, setCopied] = (0, import_react7.useState)(false);
1115
+ const copy = async () => {
1116
+ try {
1117
+ await navigator.clipboard.writeText(text);
1118
+ setCopied(true);
1119
+ window.setTimeout(() => setCopied(false), 1500);
1120
+ } catch {
1121
+ }
1122
+ };
1123
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1124
+ }
992
1125
  function TypingDots() {
993
1126
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
994
1127
  "span",
@@ -1039,11 +1172,11 @@ function ChatPanel({
1039
1172
  const { config, configError, loading } = useFrankie();
1040
1173
  const theme = useFrankieTheme();
1041
1174
  const { messages, busy, error, send, stop, reset } = useFrankieChat();
1042
- const scrollRef = (0, import_react7.useRef)(null);
1175
+ const scrollRef = (0, import_react8.useRef)(null);
1043
1176
  const features = config?.features;
1044
1177
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1045
- const [model, setModel] = (0, import_react7.useState)(void 0);
1046
- (0, import_react7.useEffect)(() => {
1178
+ const [model, setModel] = (0, import_react8.useState)(void 0);
1179
+ (0, import_react8.useEffect)(() => {
1047
1180
  const node = scrollRef.current;
1048
1181
  if (!node) return;
1049
1182
  const distanceFromBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
@@ -1072,7 +1205,11 @@ function ChatPanel({
1072
1205
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.RotateCcw, {}) }),
1073
1206
  onClose ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react4.X, {}) }) : null
1074
1207
  ] }),
1075
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: configError }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
1208
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CardContent, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollArea, { ref: scrollRef, className: "flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1209
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: configError }),
1211
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1212
+ ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-4", children: [
1076
1213
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1077
1214
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1078
1215
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-col gap-4", children: messages.map(
@@ -1123,13 +1260,13 @@ function FrankieWidget({
1123
1260
  }) {
1124
1261
  const { config } = useFrankie();
1125
1262
  const theme = useFrankieTheme();
1126
- const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react8.useState)(defaultOpen);
1263
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react9.useState)(defaultOpen);
1127
1264
  const open = controlledOpen ?? uncontrolledOpen;
1128
1265
  const setOpen = (next) => {
1129
1266
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1130
1267
  onOpenChange?.(next);
1131
1268
  };
1132
- (0, import_react8.useEffect)(() => {
1269
+ (0, import_react9.useEffect)(() => {
1133
1270
  if (!open) return;
1134
1271
  const onKeyDown = (event) => {
1135
1272
  if (event.key === "Escape") setOpen(false);
@@ -1181,10 +1318,101 @@ function FrankieInline({ className, style, ...panelProps }) {
1181
1318
  );
1182
1319
  }
1183
1320
 
1184
- // src/components/ui/separator.tsx
1321
+ // src/components/chat/FrankieChatPage.tsx
1185
1322
  var import_jsx_runtime16 = require("react/jsx-runtime");
1323
+ function PageEmptyState() {
1324
+ const { config } = useFrankie();
1325
+ const { send, busy } = useFrankieChat();
1326
+ const name = config?.branding.name ?? "Frankie";
1327
+ const prompts = config?.suggestedPrompts?.slice(0, 3) ?? [];
1328
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1329
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1330
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "frankie-page-kicker", children: [
1332
+ name,
1333
+ " assistant"
1334
+ ] }),
1335
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h1", { children: "What would you like to do?" }),
1336
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1337
+ ] }),
1338
+ prompts.length ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-prompts", children: prompts.map((prompt, index) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("button", { type: "button", disabled: busy, onClick: () => send(prompt), children: [
1339
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: String(index + 1).padStart(2, "0") }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: prompt }),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1342
+ ] }, prompt)) }) : null
1343
+ ] });
1344
+ }
1345
+ function PageChrome({
1346
+ header,
1347
+ footer,
1348
+ emptyState,
1349
+ renderMessage,
1350
+ renderTool,
1351
+ placeholder,
1352
+ autoFocus
1353
+ }) {
1354
+ const { config } = useFrankie();
1355
+ const { reset } = useFrankieChat();
1356
+ const name = config?.branding.name ?? "Frankie";
1357
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1358
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "frankie-page-header", children: [
1359
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "frankie-page-status", "aria-hidden": true }),
1361
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("strong", { children: name })
1362
+ ] }),
1363
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", onClick: reset, "aria-label": "Start a new conversation", children: "New chat" })
1364
+ ] }),
1365
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1366
+ FrankieInline,
1367
+ {
1368
+ header: header ?? false,
1369
+ footer,
1370
+ emptyState: emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PageEmptyState, {}),
1371
+ renderMessage,
1372
+ renderTool,
1373
+ placeholder,
1374
+ autoFocus
1375
+ }
1376
+ ) })
1377
+ ] });
1378
+ }
1379
+ function FrankieChatPage({
1380
+ className,
1381
+ style,
1382
+ header,
1383
+ footer,
1384
+ emptyState,
1385
+ renderMessage,
1386
+ renderTool,
1387
+ placeholder,
1388
+ autoFocus,
1389
+ ...providerProps
1390
+ }) {
1391
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1392
+ "main",
1393
+ {
1394
+ className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "),
1395
+ style: { height: "100dvh", minHeight: "100dvh", width: "100%", ...style },
1396
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FrankieProvider, { ...providerProps, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1397
+ PageChrome,
1398
+ {
1399
+ header,
1400
+ footer,
1401
+ emptyState,
1402
+ renderMessage,
1403
+ renderTool,
1404
+ placeholder,
1405
+ autoFocus
1406
+ }
1407
+ ) })
1408
+ }
1409
+ );
1410
+ }
1411
+
1412
+ // src/components/ui/separator.tsx
1413
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1186
1414
  function Separator({ className }) {
1187
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1415
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1188
1416
  }
1189
1417
  // Annotate the CommonJS export names for ESM import in node:
1190
1418
  0 && (module.exports = {
@@ -1201,6 +1429,7 @@ function Separator({ className }) {
1201
1429
  Composer,
1202
1430
  FRANKIE_CLOUD_URL,
1203
1431
  FrankieApiError,
1432
+ FrankieChatPage,
1204
1433
  FrankieClient,
1205
1434
  FrankieInline,
1206
1435
  FrankieProvider,