Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions apps/app/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,8 @@ body, html {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}

@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
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
12 changes: 12 additions & 0 deletions apps/app/src/components/generative-ui/idiomorph-inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// idiomorph v0.3.0 — https://github.com/bigskysoftware/idiomorph
// Minified source inlined for use inside sandboxed iframes (no network access).
//
// To update:
// 1. npm install idiomorph@<version> (or download from GitHub releases)
// 2. Copy the minified IIFE build into the IDIOMORPH_JS string below
// 3. Update the version comment above
// 4. Verify: the global `Idiomorph.morph(target, newContent, opts)` must be available
//
// Do NOT edit the minified code by hand.

export 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}}();`;
8 changes: 8 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 @@ -98,6 +98,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 +119,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
111 changes: 79 additions & 32 deletions apps/app/src/components/generative-ui/widget-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { z } from "zod";
import { SaveTemplateOverlay } from "./save-template-overlay";
import { IDIOMORPH_JS } from "./idiomorph-inline";

// ─── Zod Schema (CopilotKit parameter contract) ─────────────────────
export const WidgetRendererProps = z.object({
Expand Down Expand Up @@ -302,16 +303,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 Down Expand Up @@ -352,32 +358,72 @@ 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;

// 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);
});
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();
});

// Reset tracking when content is cleared (new streaming session)
if (!tmp.innerHTML.trim()) {
content.removeAttribute('data-has-content');
content.innerHTML = '';
reportHeight();
return;
}

// 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);
}

// 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');
node.addEventListener('animationend', function() {
node.classList.remove('morph-enter');
}, { once: true });
}
}
}
});
} 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 +471,7 @@ function assembleShell(initialHtml: string = ""): string {
<div id="content">
${initialHtml}
</div>
<script>${IDIOMORPH_JS}</script>
<script>
${BRIDGE_JS}
</script>
Expand Down
Loading