Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/app/src/components/generative-ui/charts/bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function BarChart({ title, description, data }: BarChartProps) {
stroke="var(--chart-axis)"
/>
<Tooltip contentStyle={CHART_CONFIG.tooltipStyle} />
<Bar isAnimationActive={false} dataKey="value" radius={[4, 4, 0, 0]} />
<Bar isAnimationActive={true} animationDuration={800} animationEasing="ease-out" dataKey="value" radius={[4, 4, 0, 0]} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/app/src/components/generative-ui/charts/pie-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ export function PieChart({ title, description, data }: PieChartProps) {
cx="50%"
cy="50%"
outerRadius={100}
isAnimationActive={false}
isAnimationActive={true}
animationDuration={800}
animationEasing="ease-out"
animationBegin={200}
/>
<Tooltip contentStyle={CHART_CONFIG.tooltipStyle} />
</RechartsPieChart>
Expand Down
14 changes: 14 additions & 0 deletions apps/app/src/components/generative-ui/meeting-time-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export function MeetingTimePicker({

return (
<div className="rounded-2xl shadow-lg max-w-md w-full border dark:border-zinc-700 mx-auto mb-6 bg-white dark:bg-zinc-800">
<style>{`
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
<div className="backdrop-blur-md p-8 w-full rounded-2xl">
{/* Show confirmation or prompt */}
{selectedSlot ? (
Expand Down Expand Up @@ -98,6 +104,10 @@ export function MeetingTimePicker({
hover:scale-102 active:scale-98
flex justify-between items-center
hover:bg-blue-50 dark:hover:bg-blue-900/30"
style={{
animation: "fadeSlideUp 0.4s ease-out both",
animationDelay: `${index * 80}ms`,
}}
>
<div className="text-left">
<div className="font-bold text-gray-900 dark:text-zinc-100">{slot.date}</div>
Expand All @@ -115,6 +125,10 @@ export function MeetingTimePicker({
text-gray-600 dark:text-zinc-400 hover:text-gray-800 dark:hover:text-zinc-200
transition-all cursor-pointer
hover:bg-gray-100 dark:hover:bg-zinc-700"
style={{
animation: "fadeSlideUp 0.4s ease-out both",
animationDelay: `${slots.length * 80}ms`,
}}
>
None of these work
</button>
Expand Down
104 changes: 72 additions & 32 deletions apps/app/src/components/generative-ui/widget-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,21 @@ input[type="checkbox"], input[type="radio"] {
a { color: var(--color-text-info); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Progressive reveal for content children */
#content > * {
/* First render: stagger all children */
#content.initial-render > * {
animation: fadeSlideIn 0.4s ease-out both;
}
#content.initial-render > *:nth-child(1) { animation-delay: 0s; }
#content.initial-render > *:nth-child(2) { animation-delay: 0.06s; }
#content.initial-render > *:nth-child(3) { animation-delay: 0.12s; }
#content.initial-render > *:nth-child(4) { animation-delay: 0.18s; }
#content.initial-render > *:nth-child(5) { animation-delay: 0.24s; }
#content.initial-render > *:nth-child(n+6) { animation-delay: 0.3s; }

/* Subsequent morphs: only new elements animate in */
.morph-enter {
animation: fadeSlideIn 0.4s ease-out both;
}
#content > *:nth-child(1) { animation-delay: 0s; }
#content > *:nth-child(2) { animation-delay: 0.06s; }
#content > *:nth-child(3) { animation-delay: 0.12s; }
#content > *:nth-child(4) { animation-delay: 0.18s; }
#content > *:nth-child(5) { animation-delay: 0.24s; }
#content > *:nth-child(n+6) { animation-delay: 0.3s; }

@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(8px); }
Expand All @@ -326,6 +331,11 @@ a:hover { text-decoration: underline; }
}
`;

// ─── Injected JS: Idiomorph DOM Morphing Library (Layer 0) ───────────
// idiomorph v0.3.0 — https://github.com/bigskysoftware/idiomorph
// Inlined to avoid CDN network request inside sandboxed iframe.
const IDIOMORPH_JS = `var Idiomorph=function(){"use strict";let o=new Set;let n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:t,afterHeadMorphed:t}};function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=k(t)}let l=y(t);let r=p(e,l,n);return a(e,l,r)}function a(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=c(n,t,o);Promise.all(e).then(function(){a(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=d(r,e,o);if(e){return N(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function u(e,t){return t.ignoreActiveValue&&e===document.activeElement}function d(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return e;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!g(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return e;if(n.callbacks.beforeNodeAdded(t)===false)return e;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return e;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){c(t,e,n)}else{r(t,e,n);if(!u(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let a;while(i){a=i;i=a.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(a)===false)return;l.appendChild(a);r.callbacks.afterNodeAdded(a);H(r,a);continue}if(b(a,o,r)){d(o,a,r);o=o.nextSibling;H(r,a);continue}let e=A(n,l,a,o,r);if(e){o=v(o,e,r);d(e,a,r);H(r,a);continue}let t=S(n,l,a,o,r);if(t){o=v(o,t,r);d(t,a,r);H(r,a);continue}if(r.callbacks.beforeNodeAdded(a)===false)return;l.insertBefore(a,o);r.callbacks.afterNodeAdded(a);H(r,a)}while(o!==null){let e=o;o=o.nextSibling;T(e,r)}}function f(e,t,n,l){if(e==="value"&&l.ignoreActiveValue&&t===document.activeElement){return true}return l.callbacks.beforeAttributeUpdated(e,t,n)===false}function r(t,n,l){let e=t.nodeType;if(e===1){const r=t.attributes;const i=n.attributes;for(const o of r){if(f(o.name,n,"update",l)){continue}if(n.getAttribute(o.name)!==o.value){n.setAttribute(o.name,o.value)}}for(let e=i.length-1;0<=e;e--){const a=i[e];if(f(a.name,n,"remove",l)){continue}if(!t.hasAttribute(a.name)){n.removeAttribute(a.name)}}}if(e===8||e===3){if(n.nodeValue!==t.nodeValue){n.nodeValue=t.nodeValue}}if(!u(n,l)){s(t,n,l)}}function i(t,n,l,r){if(t[l]!==n[l]){let e=f(l,n,"update",r);if(!e){n[l]=t[l]}if(t[l]){if(!e){n.setAttribute(l,t[l])}}else{if(!f(l,n,"remove",r)){n.removeAttribute(l)}}}}function s(n,l,r){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){let e=n.value;let t=l.value;i(n,l,"checked",r);i(n,l,"disabled",r);if(!n.hasAttribute("value")){if(!f("value",l,"remove",r)){l.value="";l.removeAttribute("value")}}else if(e!==t){if(!f("value",l,"update",r)){l.setAttribute("value",e);l.value=e}}}else if(n instanceof HTMLOptionElement){i(n,l,"selected",r)}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(f("value",l,"update",r)){return}if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function c(e,t,l){let r=[];let i=[];let o=[];let a=[];let u=l.head.style;let d=new Map;for(const n of e.children){d.set(n.outerHTML,n)}for(const s of t.children){let e=d.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{d.delete(s.outerHTML);o.push(s)}}else{if(u==="append"){if(t){i.push(s);a.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}a.push(...d.values());m("to append: ",a);let f=[];for(const c of a){m("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;m(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});f.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return f}function m(){}function t(){}function h(e){let t={};Object.assign(t,n);Object.assign(t,e);t.callbacks={};Object.assign(t.callbacks,n.callbacks);Object.assign(t.callbacks,e.callbacks);t.head={};Object.assign(t.head,n.head);Object.assign(t.head,e.head);return t}function p(e,t,n){n=h(n);return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:C(e,t),deadIds:new Set,callbacks:n.callbacks,head:n.head}}function b(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return L(n,e,t)>0}}return false}function g(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function v(t,e,n){while(t!==e){let e=t;t=t.nextSibling;T(e,n)}H(n,e);return e.nextSibling}function A(n,e,l,r,i){let o=L(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(b(l,e,i)){return e}t+=L(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function S(e,t,n,l,r){let i=l;let o=n.nextSibling;let a=0;while(i!=null){if(L(r,i,e)>0){return null}if(g(n,i)){return i}if(g(o,i)){a++;o=o.nextSibling;if(a>=2){return null}}i=i.nextSibling}return i}function k(n){let l=new DOMParser;let e=n.replace(/<svg(\\s[^>]*>|>)([\\s\\S]*?)<\\/svg>/gim,"");if(e.match(/<\\/html>/)||e.match(/<\\/head>/)||e.match(/<\\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function y(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function N(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=w(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function w(e,t,n){if(g(e,t)){return.5+L(n,e,t)}return 0}function T(e,t){H(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function E(e,t){return!e.deadIds.has(t)}function x(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function H(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function L(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(E(e,i)&&x(e,i,n)){++r}}return r}function R(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function C(e,t){let n=new Map;R(e,n);R(t,n);return n}return{morph:e,defaults:n}}();`;

// ─── Injected JS: Bridge Functions + Auto-Resize (Layers 1 & 6) ─────
const BRIDGE_JS = `
// Bridge: sendPrompt
Expand All @@ -352,32 +362,61 @@ window.addEventListener('message', function(e) {
if (e.source !== window.parent) return;
if (e.data && e.data.type === 'update-content') {
var content = document.getElementById('content');
if (content) {
// Strip script tags from HTML before inserting — scripts are handled separately below
var tmp = document.createElement('div');
tmp.innerHTML = e.data.html;
var incomingScripts = [];
tmp.querySelectorAll('script').forEach(function(s) {
incomingScripts.push({ src: s.src, text: s.textContent });
s.remove();
});
content.innerHTML = tmp.innerHTML;
if (!content) return;

// Strip script tags from HTML before inserting — scripts are handled separately below
var tmp = document.createElement('div');
tmp.innerHTML = e.data.html;
var incomingScripts = [];
tmp.querySelectorAll('script').forEach(function(s) {
incomingScripts.push({ src: s.src, text: s.textContent });
s.remove();
});

// First render: add stagger class for initial entrance animation
var isFirstRender = !content.hasAttribute('data-has-content');
if (isFirstRender) {
content.classList.add('initial-render');
content.setAttribute('data-has-content', '1');
setTimeout(function() { content.classList.remove('initial-render'); }, 800);
}

// Execute only new scripts (not previously executed)
incomingScripts.forEach(function(scriptInfo) {
var key = scriptInfo.src || scriptInfo.text;
if (content.getAttribute('data-exec-' + btoa(key).slice(0, 16))) return;
content.setAttribute('data-exec-' + btoa(key).slice(0, 16), '1');
var newScript = document.createElement('script');
if (scriptInfo.src) {
newScript.src = scriptInfo.src;
} else {
newScript.textContent = scriptInfo.text;
}
content.appendChild(newScript);
});
reportHeight();
// Use idiomorph to diff/patch DOM (preserves existing nodes, no flicker)
if (window.Idiomorph) {
try {
Idiomorph.morph(content, tmp.innerHTML, {
morphStyle: 'innerHTML',
callbacks: {
beforeNodeAdded: function(node) {
// Tag new element nodes for entrance animation
if (node.nodeType === 1) {
node.classList.add('morph-enter');
}
}
}
});
} catch (err) {
// Fallback: full replacement on morph failure
content.innerHTML = tmp.innerHTML;
}
} else {
content.innerHTML = tmp.innerHTML;
}

// Execute only new scripts (not previously executed)
incomingScripts.forEach(function(scriptInfo) {
var key = scriptInfo.src || scriptInfo.text;
if (content.getAttribute('data-exec-' + btoa(key).slice(0, 16))) return;
content.setAttribute('data-exec-' + btoa(key).slice(0, 16), '1');
var newScript = document.createElement('script');
if (scriptInfo.src) {
newScript.src = scriptInfo.src;
} else {
newScript.textContent = scriptInfo.text;
}
content.appendChild(newScript);
});
reportHeight();
}
});

Expand Down Expand Up @@ -425,6 +464,7 @@ function assembleShell(initialHtml: string = ""): string {
<div id="content">
${initialHtml}
</div>
<script>${IDIOMORPH_JS}</script>
<script>
${BRIDGE_JS}
</script>
Expand Down
Loading