diff --git a/app/internal/agent-evals/page.tsx b/app/internal/agent-evals/page.tsx index 71ca7e5..1ad16f0 100644 --- a/app/internal/agent-evals/page.tsx +++ b/app/internal/agent-evals/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { AgentEvalDashboard } from "@/components/agent-eval-dashboard"; +import { createAgentV3PlatformEvidence } from "@/lib/agent/evals/dashboard-evidence"; export const metadata: Metadata = { title: "Agent evals · Drops Studio", @@ -8,6 +9,7 @@ export const metadata: Metadata = { robots: { index: false, follow: false }, }; -export default function AgentEvalsPage() { - return ; +export default async function AgentEvalsPage() { + const platform = await createAgentV3PlatformEvidence({ env: process.env }); + return ; } diff --git a/app/layout.tsx b/app/layout.tsx index dd91cb0..fe25745 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -56,7 +56,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/app/page.tsx b/app/page.tsx index af6fa8f..22d1b5e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,8 @@ import { DropsStudio } from "@/components/drops-studio"; -import { Sparkles } from "lucide-react"; function LandingHero() { return (
- - DROPS AI CRYPTO BUILDER -

Build crypto apps{" "}
diff --git a/app/styles/drops-brand.css b/app/styles/drops-brand.css index 9d5f5ca..0af2f92 100644 --- a/app/styles/drops-brand.css +++ b/app/styles/drops-brand.css @@ -11,15 +11,14 @@ display: flex; flex: 0 0 auto; isolation: isolate; - min-width: 54px; + min-width: 40px; } -.drops-brand-dropstab, -.drops-brand-bot { +.drops-brand-dropstab { border: 2px solid #fff; box-shadow: 0 8px 22px rgba(34, 79, 165, 0.18); - height: 38px; - width: 38px; + height: 40px; + width: 40px; } .drops-brand-dropstab { @@ -37,16 +36,8 @@ width: auto; } -.drops-brand-bot { - border-radius: 12px; - margin-left: -10px; - object-fit: cover; - position: relative; - z-index: 1; -} - .studio-header .drops-brand-marks { - min-width: 54px; + min-width: 40px; } .drops-brand-copy { @@ -57,42 +48,17 @@ .drops-brand-copy > strong { color: #07142f; - font-size: 20px; - font-weight: 760; - letter-spacing: -0.45px; + font-size: 19px; + font-weight: 780; + letter-spacing: -0.5px; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.drops-brand-partners { - align-items: center; - color: #66758d; - display: flex; - flex-wrap: wrap; - font-size: 12px; - gap: 5px; - line-height: 1.2; -} - -.drops-brand-partners > span { - align-items: center; - display: inline-flex; - font-weight: 680; - gap: 4px; -} - -.drops-brand-partners img { - border-radius: 4px; - height: 14px; - object-fit: contain; - width: auto; -} - -.drops-brand-partners i { - color: #a0aabc; - font-style: normal; +.drops-brand-copy > strong span { + color: #245fe5; } .drops-brand-compact { @@ -100,14 +66,13 @@ } .drops-brand-compact .drops-brand-marks { - min-width: 46px; + min-width: 34px; } -.drops-brand-compact .drops-brand-dropstab, -.drops-brand-compact .drops-brand-bot { +.drops-brand-compact .drops-brand-dropstab { border-radius: 10px; - height: 32px; - width: 32px; + height: 34px; + width: 34px; } .drops-brand-compact .drops-brand-dropstab img { @@ -115,25 +80,13 @@ width: auto; } -.drops-brand-compact .drops-brand-bot { - margin-left: -8px; -} - .drops-brand-compact .drops-brand-copy > strong { - font-size: 14px; + font-size: 15px; } @media (max-width: 520px) { .studio-header .drops-brand-marks { - min-width: 54px; - } - - .studio-header .drops-brand-bot { - display: block; - } - - .drops-brand:not(.drops-brand-compact) .drops-brand-partners { - display: none; + min-width: 40px; } .drops-brand-copy > strong { diff --git a/app/styles/drops-studio.builder.css b/app/styles/drops-studio.builder.css index 7c2d16e..4bb4413 100644 --- a/app/styles/drops-studio.builder.css +++ b/app/styles/drops-studio.builder.css @@ -4,9 +4,9 @@ .builder-column > .preset-section { grid-column: 1 / -1; grid-row: 2; min-width: 0; } .builder-column > .setup-card { grid-column: 1 / -1; grid-row: 3; min-width: 0; } .studio-grid > .preview-column { grid-column: 2; grid-row: 1; } -.hero-copy { margin-bottom: 28px; } +.hero-copy { margin-bottom: 28px; padding-top: 8px; } .eyebrow { align-items: center; color: var(--blue); display: flex; font-size: 12px; font-weight: 760; gap: 7px; letter-spacing: .13em; margin-bottom: 18px; } -.hero-copy h1 { font-size: clamp(48px, 4.4vw, 68px); letter-spacing: -.065em; line-height: .98; margin: 0; max-width: 790px; } +.hero-copy h1 { font-size: clamp(44px, 3.8vw, 60px); letter-spacing: -.058em; line-height: 1.01; margin: 0; max-width: 730px; text-wrap: balance; } .hero-copy h1 span { color: var(--blue); } .hero-description { display: grid; gap: 2px; margin: 22px 0 0; max-width: 690px; } .hero-copy .hero-description p { color: var(--muted); font-size: 16px; line-height: 1.62; margin: 0; max-width: 690px; } @@ -33,7 +33,8 @@ .carousel-controls button { align-items: center; background: white; border: 1px solid var(--line); border-radius: 10px; cursor: pointer; display: flex; height: 44px; justify-content: center; width: 44px; } .carousel-controls button:hover { border-color: #abc3ff; color: var(--blue); } .carousel-controls svg { height: 16px; width: 16px; } -.preset-carousel { display: flex; gap: 12px; margin: 0 -4px; overflow-x: auto; padding: 4px 4px 18px; scroll-behavior: smooth; scroll-snap-type: x mandatory; scrollbar-color: #d8e1f1 transparent; scrollbar-width: thin; } +.preset-carousel { display: flex; gap: 12px; margin: 0 -4px; overflow-x: auto; padding: 4px 4px 18px; scroll-behavior: smooth; scroll-snap-type: x mandatory; scrollbar-width: none; } +.preset-carousel::-webkit-scrollbar { display: none; } .preset-card { background: rgba(255, 255, 255, .82); border: 1px solid var(--line); border-radius: 16px; cursor: pointer; display: flex; flex: 0 0 172px; flex-direction: column; min-height: 176px; padding: 17px; position: relative; scroll-snap-align: center; text-align: left; transition: border-color .2s, box-shadow .2s, transform .2s; } .preset-card:hover { border-color: color-mix(in srgb, var(--preset-accent) 45%, white); transform: translateY(-2px); } .preset-card.selected { background: white; border: 1.5px solid var(--preset-accent); box-shadow: 0 14px 30px color-mix(in srgb, var(--preset-accent) 14%, transparent); flex-basis: 204px; } diff --git a/app/styles/drops-studio.previews.css b/app/styles/drops-studio.previews.css index e665873..69309ac 100644 --- a/app/styles/drops-studio.previews.css +++ b/app/styles/drops-studio.previews.css @@ -149,13 +149,11 @@ .siri-suggestions { flex-wrap: wrap; justify-content: center; } .siri-suggestions button, .mic-button { min-height: 44px; } -/* Category-native preview surfaces: the chosen product, never a renamed generic card. */ .native-screen-tabs { align-items: center; background: rgba(255,255,255,.86); border-bottom: 1px solid #dfe7f2; display: flex; gap: 6px; min-height: 46px; overflow-x: auto; padding: 8px 18px; position: relative; z-index: 2; }.native-screen-tabs span { border: 1px solid transparent; border-radius: 8px; color: #738096; flex: 0 0 auto; font-size: 12px; font-weight: 650; padding: 7px 9px; }.native-screen-tabs span.active { background: #edf3ff; border-color: #cbdcff; color: #245fe8; } .preview-device:not(.telegram-native):not(.game-native) .preview-stage { align-items: stretch; padding: 24px; }.preview-device:not(.telegram-native):not(.game-native) .preview-stage > * { max-width: none; width: 100%; }.preview-device:not(.telegram-native):not(.game-native) .engine-card, .preview-device:not(.telegram-native):not(.game-native) .prediction-card, .preview-device:not(.telegram-native):not(.game-native) .copy-card, .preview-device:not(.telegram-native):not(.game-native) .aggregator-card, .preview-device:not(.telegram-native):not(.game-native) .companion-card, .preview-device:not(.telegram-native):not(.game-native) .tamagotchi-card, .preview-device:not(.telegram-native):not(.game-native) .hunt-card, .preview-device:not(.telegram-native):not(.game-native) .radio-card, .preview-device:not(.telegram-native):not(.game-native) .siri-card { box-shadow: 0 12px 30px rgba(36,65,112,.09); } .telegram-native { background: #d9ecff; }.telegram-native::before { background-image: radial-gradient(circle at center, rgba(41,121,214,.13) 1px, transparent 1px), linear-gradient(45deg, transparent 48%, rgba(255,255,255,.22) 49%, rgba(255,255,255,.22) 51%, transparent 52%); background-size: 22px 22px, 68px 68px; opacity: .6; }.telegram-native .preview-device-header { height: 84px; }.preview-device-header > .telegram-back { align-items: center; color: #2481cc; display: flex; flex: 0 0 auto; margin-left: -10px; margin-right: 6px; padding: 8px; }.preview-device-header > .telegram-back svg { height: 22px; width: 22px; }.telegram-native .preview-stage { align-items: flex-start; min-height: 515px; padding: 30px 30px 18px; }.telegram-native .telegram-card { border-radius: 20px; box-shadow: 0 16px 38px rgba(55,94,143,.16); max-width: 470px; padding: 24px; }.telegram-native .message-title strong { font-size: 16px; }.telegram-native .message-title span, .telegram-native .alpha-token-row span { font-size: 12px; }.telegram-native .alpha-token-row strong { font-size: 14px; }.telegram-native .alpha-copy, .telegram-native .brief-block p { font-size: 12px; line-height: 1.58; }.telegram-native .brief-heading strong, .telegram-native .brief-heading b { font-size: 13px; }.telegram-native .brief-block button { font-size: 12px; }.telegram-native .preview-reactions { margin-bottom: 20px; } .game-native { background: #191426; border-color: #4e3642; }.game-native::before { display: none; }.game-native .preview-device-header { background: #15162a; border-bottom-color: rgba(255,255,255,.1); color: white; }.game-native .preview-device-header span { color: #aab1c3; }.game-native .preview-stage { display: block; min-height: 596px; padding: 0; }.catcher-game { background: radial-gradient(circle at 50% 15%, #6268a4, #17172f 72%); background-position: center; background-size: cover; color: white; height: 596px; isolation: isolate; overflow: hidden; position: relative; width: 100%; }.catcher-game::after { background: linear-gradient(180deg, rgba(8,8,20,.38), transparent 28%, transparent 65%, rgba(11,7,18,.7)); content: ""; inset: 0; pointer-events: none; position: absolute; z-index: -1; }.catcher-hud { align-items: center; display: grid; gap: 8px; grid-template-columns: 1fr auto auto auto; left: 0; padding: 15px 17px; position: absolute; right: 0; top: 0; z-index: 5; }.catcher-hud > span { align-items: center; background: rgba(8,11,23,.72); border: 1px solid rgba(255,255,255,.22); border-radius: 99px; display: flex; font-size: 12px; font-weight: 800; gap: 5px; padding: 7px 9px; width: max-content; }.catcher-hud svg { height: 13px; width: 13px; }.catcher-hud > div { align-items: center; background: rgba(8,11,23,.72); border: 1px solid rgba(255,255,255,.18); border-radius: 9px; display: flex; flex-direction: column; min-width: 62px; padding: 6px 9px; }.catcher-hud b { font-size: 12px; }.catcher-hud small { color: #c8c8d5; font-size: 12px; letter-spacing: .08em; }.catcher-hud > strong { background: #ffe073; border: 2px solid #382719; border-radius: 50%; color: #35220e; display: grid; font-size: 12px; height: 42px; place-items: center; width: 42px; }.catcher-title { left: 20px; max-width: 300px; position: absolute; top: 76px; z-index: 4; }.catcher-title > span { background: #e84e43; border: 2px solid #52281e; border-radius: 5px; box-shadow: 3px 3px 0 #52281e; display: inline-block; font-size: 12px; font-weight: 900; letter-spacing: .08em; padding: 6px 8px; }.catcher-title h3 { color: #fff4ce; font-family: var(--font-geist-sans), sans-serif; font-size: 34px; letter-spacing: -.05em; line-height: .92; margin: 12px 0 7px; max-width: 280px; text-shadow: 3px 3px 0 #5e3026; }.catcher-title p { background: rgba(35,22,20,.7); border-radius: 7px; color: #fff3dc; font-size: 12px; line-height: 1.45; margin: 0; padding: 7px 9px; width: max-content; max-width: 285px; }.catcher-start { align-items: center; background: #ffd45b; border: 3px solid #4c2d1c; border-radius: 12px; bottom: 78px; box-shadow: 5px 6px 0 rgba(65,32,20,.88); color: #3a2316; cursor: pointer; display: flex; font-size: 15px; font-weight: 900; gap: 8px; left: 50%; min-height: 51px; padding: 0 23px; position: absolute; transform: translateX(-50%); z-index: 6; }.catcher-start:hover { transform: translate(-50%, -2px); }.catcher-character-marker { bottom: 65px; position: absolute; transition: left .2s ease; z-index: 5; }.catcher-character-marker i { background: rgba(255,218,104,.22); border: 3px solid #ffe27a; border-radius: 50%; box-shadow: 0 0 0 6px rgba(52,32,20,.25); display: block; height: 44px; width: 72px; }.catcher-character-marker span { background: #35251f; border-radius: 4px; display: block; font-size: 12px; font-weight: 900; margin: 4px auto 0; padding: 3px 5px; text-align: center; width: max-content; }.falling-layer { inset: 0; overflow: hidden; pointer-events: none; position: absolute; z-index: 3; }.falling-token { align-items: center; animation: token-fall 2.6s linear infinite; background: #37b58b; border: 3px solid #134e3e; border-radius: 48% 52% 46% 54%; box-shadow: inset -4px -5px 0 rgba(0,0,0,.13), 3px 5px 0 rgba(64,35,22,.45); display: flex; font-size: 12px; font-style: normal; font-weight: 900; height: 44px; justify-content: center; position: absolute; top: 4%; width: 34px; }.falling-token.lane-0 { left: 31%; }.falling-token.lane-1 { animation-duration: 3.1s; background: #e4ad35; border-color: #64420f; left: 50%; }.falling-token.lane-2 { animation-duration: 2.3s; background: #368ed8; border-color: #183f6a; left: 68%; }.falling-token.lane-3 { left: 84%; }.falling-token.hazard { animation-duration: 2.8s; background: #e94f42; border-radius: 50%; border-color: #6b1f1b; color: white; font-size: 18px; }.catcher-controls { align-items: center; bottom: 12px; display: flex; gap: 12px; justify-content: center; left: 0; position: absolute; right: 0; z-index: 7; }.catcher-controls button { align-items: center; background: rgba(24,17,22,.86); border: 2px solid rgba(255,237,190,.8); border-radius: 10px; color: white; cursor: pointer; display: flex; height: 42px; justify-content: center; width: 52px; }.catcher-controls span { background: rgba(24,17,22,.75); border-radius: 99px; color: #fff1ca; font-size: 12px; padding: 7px 10px; }.catcher-result { align-items: center; background: rgba(23,15,20,.9); border: 2px solid #ffe37a; border-radius: 16px; bottom: 64px; display: flex; flex-direction: column; left: 50%; min-width: 220px; padding: 18px; position: absolute; transform: translateX(-50%); z-index: 8; }.catcher-result > svg { color: #ffe37a; }.catcher-result strong { font-size: 21px; margin: 6px 0 3px; }.catcher-result span { color: #d5ccbc; font-size: 12px; }.catcher-result button { align-items: center; background: #ffd45b; border: 0; border-radius: 8px; color: #352315; display: flex; font-size: 12px; font-weight: 800; gap: 5px; margin-top: 11px; padding: 8px 11px; }@keyframes token-fall { from { transform: translateY(-40px) rotate(-8deg); } to { transform: translateY(510px) rotate(15deg); } } -/* Telegram output copy follows the same legible scale as the published post. */ .telegram-native .alpha-copy, .telegram-native .brief-block p, .telegram-native .brief-heading strong, @@ -270,9 +268,15 @@ font-size: 14px; } -.studio-footer { align-items: center; border-top: 1px solid var(--line); display: flex; gap: 28px; justify-content: space-between; margin: 0 auto; max-width: 1500px; padding: 30px 44px 40px; position: relative; z-index: 2; }.studio-footer .brand-lockup { transform: scale(.9); transform-origin: left; }.studio-footer p { color: #7b879a; font-size: 12px; }.studio-footer > div:last-child { display: flex; gap: 16px; }.studio-footer a { color: #5f6c82; font-size: 12px; font-weight: 620; text-decoration: none; }.studio-footer a:hover { color: var(--blue); } +.studio-footer { border-top: 1px solid var(--line); display: grid; gap: 30px; grid-template-columns: minmax(220px,.8fr) minmax(0,1.4fr); margin: 0 auto; max-width: 1500px; padding: 42px 44px 28px; position: relative; z-index: 2; } +.studio-footer-intro { align-items: flex-start; display: flex; flex-direction: column; gap: 16px; min-width: 0; } +.studio-footer-intro p { color: #67758a; font-size: 14px; line-height: 1.6; margin: 0; max-width: 360px; } +.studio-footer-links { display: grid; gap: 28px; grid-template-columns: repeat(3,minmax(120px,1fr)); } +.studio-footer-links > div { align-items: flex-start; display: flex; flex-direction: column; min-width: 0; } +.studio-footer-links strong { color: #14213a; font-size: 13px; letter-spacing: -.01em; margin-bottom: 8px; } +.studio-footer-bottom { align-items: center; border-top: 1px solid #e5ebf4; color: #596980; display: flex; font-size: 12px; gap: 18px; grid-column: 1 / -1; justify-content: space-between; padding-top: 20px; } +.studio-footer a { color: #5f6c82; display: inline-flex; font-size: 13px; font-weight: 640; min-height: 44px; min-width: 44px; text-decoration: none; }.studio-footer a:hover { color: var(--blue); } -/* Category-native preview controls remain fully tappable at every viewport. */ .game-lock, .brief-block button, .catcher-result button, @@ -293,7 +297,6 @@ align-items: center; display: inline-flex; min-height: 44px; - min-width: 44px; } @media (prefers-reduced-motion: reduce) { @@ -455,8 +458,7 @@ overflow: hidden; } -.landing-studio-toolbar, -.landing-studio-statusbar { +.landing-studio-toolbar { align-items: center; background: rgba(255, 255, 255, .97); display: flex; @@ -474,8 +476,7 @@ .landing-studio-toolbar button, .landing-copilot-title, .landing-copilot-card, -.landing-studio-copilot > button, -.landing-studio-statusbar span { +.landing-studio-copilot > button { align-items: center; display: flex; } @@ -541,15 +542,14 @@ } .landing-studio-toolbar button svg, -.landing-studio-copilot > button svg, -.landing-studio-statusbar svg { +.landing-studio-copilot > button svg { height: 14px; width: 14px; } .landing-studio-body { display: grid; - grid-template-columns: 82px minmax(0, 1fr) 190px; + grid-template-columns: 82px minmax(0, 1fr) minmax(190px, 220px); min-height: 510px; } @@ -764,28 +764,15 @@ margin-top: auto; min-height: 44px; padding: 0 9px; + text-align: center; + white-space: normal; } -.landing-studio-copilot > button svg:last-child { - margin-left: auto; -} - -.landing-studio-statusbar { - border-top: 1px solid #e4eaf3; - color: #65738a; - font-size: 12px; - gap: 10px; - min-height: 40px; - padding: 7px 13px; -} - -.landing-studio-statusbar span { - gap: 5px; -} - -.landing-studio-statusbar span:first-child { - color: #087449; -} +.landing-copilot-title { flex-wrap: wrap; min-width: 0; } +.landing-copilot-title strong { min-width: 0; } +.landing-copilot-title span { flex: 0 0 auto; margin-left: auto; max-width: 100%; } +.landing-studio-copilot,.landing-copilot-card,.landing-copilot-card > div { min-width: 0; } +.landing-copilot-card :is(strong,span),.landing-studio-copilot > p { overflow-wrap: anywhere; } @media (max-width: 1260px) { .landing-studio-body { @@ -817,8 +804,7 @@ .landing-studio-toolbar > div:first-child > b, .landing-studio-actions button:first-child, .landing-studio-rail, - .landing-studio-copilot, - .landing-studio-statusbar span:not(:first-child) { + .landing-studio-copilot { display: none; } @@ -834,7 +820,15 @@ font-size: 12px; } - .landing-studio-statusbar { - justify-content: center; - } +} + +@media (max-width: 780px) { + .studio-footer { grid-template-columns: 1fr; } + .studio-footer-links { grid-template-columns: repeat(3,minmax(0,1fr)); } + .studio-footer-bottom { grid-column: 1; } +} + +@media (max-width: 560px) { + .studio-footer-links { grid-template-columns: 1fr 1fr; } + .studio-footer-bottom { align-items: flex-start; flex-direction: column; } } diff --git a/app/styles/drops-studio.responsive.css b/app/styles/drops-studio.responsive.css index 0301ed5..349362a 100644 --- a/app/styles/drops-studio.responsive.css +++ b/app/styles/drops-studio.responsive.css @@ -1,6 +1,6 @@ @media (max-width: 1160px) { .studio-grid { grid-template-columns: minmax(0, .9fr) minmax(540px, 1.1fr); } - .hero-copy h1 { font-size: 52px; } + .hero-copy h1 { font-size: clamp(44px, 3.8vw, 50px); } .field-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .tool-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .preview-device { min-height: 640px; } @@ -24,8 +24,6 @@ .builder-column > .preset-section { order: 3; } .builder-column > .setup-card { order: 4; } .preview-device { margin: 0 auto; max-width: 580px; } - .preview-status-row { margin-left: auto; margin-right: auto; max-width: 580px; } - .preview-footnote { margin-left: auto; margin-right: auto; max-width: 580px; } .studio-footer { padding-left: 22px; padding-right: 22px; } } @@ -60,7 +58,6 @@ .tool-grid { grid-template-columns: 1fr; } .source-strip { align-items: flex-start; flex-direction: column; } .source-strip > div span { white-space: normal; } - .preview-status-row { align-items: flex-start; flex-direction: column; gap: 8px; } .preview-device { border-radius: 19px; min-height: 590px; } .preview-device-header { height: 70px; padding: 0 16px; } .preview-brand-mark { height: 39px; width: 39px; } @@ -75,8 +72,7 @@ .aggregator-card, .tamagotchi-card { padding: 0; } .preview-actions { flex-wrap: wrap; } .preview-action { min-width: 28%; } - .studio-footer { align-items: flex-start; flex-direction: column; gap: 12px; } - .studio-footer .brand-lockup { transform: none; } + .studio-footer { gap: 26px; } .connection-layout { display: block; } .provider-list { border-bottom: 1px solid var(--line); border-right: 0; flex-direction: row; overflow-x: auto; } .provider-list button { flex: 0 0 145px; } diff --git a/app/styles/project-studio.inspector.css b/app/styles/project-studio.inspector.css index 8d1562e..c5823af 100644 --- a/app/styles/project-studio.inspector.css +++ b/app/styles/project-studio.inspector.css @@ -1,4 +1,7 @@ -.studio-inspector { background: white; border-right: 1px solid var(--ps-line); overflow-y: auto; padding: 18px 16px 28px; } +.studio-inspector { background: white; border-right: 1px solid var(--ps-line); overflow-y: auto; overscroll-behavior: contain; padding: 18px 16px 28px; scrollbar-color: #b8c5d7 transparent; scrollbar-width: thin; } +.studio-inspector::-webkit-scrollbar { width: 8px; } +.studio-inspector::-webkit-scrollbar-track { background: transparent; } +.studio-inspector::-webkit-scrollbar-thumb { background: #b8c5d7; border: 2px solid transparent; border-radius: 999px; background-clip: padding-box; } .inspector-section { animation: inspector-in .18s ease; } @keyframes inspector-in { from { opacity: .45; transform: translateX(-3px); } } .inspector-heading { align-items: center; display: flex; justify-content: space-between; margin-bottom: 13px; } diff --git a/app/styles/project-studio.runtime.css b/app/styles/project-studio.runtime.css index 6eff371..536559c 100644 --- a/app/styles/project-studio.runtime.css +++ b/app/styles/project-studio.runtime.css @@ -1,13 +1,20 @@ -.runtime-stage { background: #eef2f8; min-width: 0; overflow: auto; padding: 12px; } +.runtime-stage { background: #eef2f8; min-width: 0; overflow: hidden; padding: 10px; } .stage-toolbar { align-items: center; display: flex; height: 37px; justify-content: space-between; margin-bottom: 9px; } -.stage-toolbar > div { align-items: center; display: flex; gap: 6px; } +.stage-toolbar > div { align-items: center; display: flex; gap: 6px; min-width: 0; } +.stage-toolbar > div:last-child { overflow-x: auto; scrollbar-width: none; } +.stage-toolbar > div:last-child::-webkit-scrollbar { display: none; } .stage-toolbar button { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 7px; color: #718199; display: flex; font-size: 12px; font-weight: 710; gap: 5px; min-height: 29px; padding: 0 8px; } .stage-toolbar button:hover,.stage-toolbar button.active { background: white; border-color: #d4deeb; color: #2f63c8; } .stage-toolbar button.quality-ready { background: #e8f8f1; border-color: #bfe4d4; color: #16875b; } .stage-toolbar svg { height: 12px; width: 12px; } .device-switch { background: #e3e9f2; border-radius: 8px; padding: 2px; } .device-switch button.active { box-shadow: 0 2px 5px rgba(28,46,75,.1); } -.runtime-browser { background: white; border: 1px solid #d4dfec; border-radius: 14px; box-shadow: 0 14px 36px rgba(28,48,80,.1); height: calc(100% - 46px); margin: auto; min-height: 590px; overflow: auto; transition: .2s ease; width: 100%; } +.canvas-zoom-controls { align-items: center; background: #e3e9f2; border-radius: 8px; display: inline-flex; flex: 0 0 auto; padding: 2px; } +.canvas-zoom-controls button { justify-content: center; min-width: 38px; padding-inline: 7px; } +.canvas-zoom-controls button:nth-child(2) { min-width: 58px; } +.runtime-canvas-scroll { height: calc(100% - 46px); min-height: 590px; overflow: auto; overscroll-behavior: contain; scrollbar-color: #b9c5d5 transparent; scrollbar-width: thin; width: 100%; } +.runtime-canvas-viewport { height: 100%; margin-inline: auto; min-width: 100%; transform-origin: left top; } +.runtime-browser { background: white; border: 1px solid #d4dfec; border-radius: 14px; box-shadow: 0 14px 36px rgba(28,48,80,.1); height: 100%; margin: auto; min-height: 590px; overflow: auto; transition: .2s ease; width: 100%; } .runtime-browser.mobile { max-width: 420px; } .browser-bar { align-items: center; background: #fafbfd; border-bottom: 1px solid #e3e9f1; display: grid; grid-template-columns: 1fr auto 1fr; height: 35px; padding: 0 10px; position: sticky; top: 0; z-index: 2; } .browser-bar > span { display: flex; gap: 4px; }.browser-bar > span i { background: #bdc8d7; border-radius: 50%; height: 6px; width: 6px; }.browser-bar > strong { background: white; border: 1px solid #dfe6ef; border-radius: 7px; color: #7d8b9f; font-size: 12px; font-weight: 650; min-width: 160px; padding: 5px 20px; text-align: center; }.browser-bar > b { align-items: center; color: #749086; display: flex; font-size: 12px; gap: 5px; justify-self: end; }.browser-bar > b i { background: #20b479; border-radius: 50%; box-shadow: 0 0 7px #20b479; height: 5px; width: 5px; }.runtime-ready-label { display: none; } diff --git a/app/styles/project-studio.workspace.css b/app/styles/project-studio.workspace.css index e817188..fe9ed30 100644 --- a/app/styles/project-studio.workspace.css +++ b/app/styles/project-studio.workspace.css @@ -26,8 +26,8 @@ .file-tree span,.file-tree button { font-size: 12px; padding: 12px; }.file-tree b { font-size: 12px; }.git-card { border-radius: 13px; padding: 14px; }.git-card strong { font-size: 12px; }.git-card small { font-size: 12px; }.git-card p { font-size: 12px; }.git-card button { font-size: 12px; min-height: 40px; } .quality-hero { padding: 14px; }.quality-hero strong { font-size: 13px; }.quality-hero small { font-size: 12px; }.quality-list > div { padding: 11px; }.quality-list strong { font-size: 12px; }.quality-list small { font-size: 12px; }.quality-list b { font-size: 12px; } .checkpoint-list { gap: 9px; }.checkpoint-list > button { padding: 12px; }.checkpoint-list strong { font-size: 12px; }.checkpoint-list small { font-size: 12px; }.checkpoint-list > button > b { font-size: 12px; } -.runtime-stage { background: #eef2f8; overflow: auto; padding: 13px 16px 16px; }.stage-toolbar { height: 44px; margin-bottom: 10px; }.stage-toolbar button { font-size: 12px; min-height: 44px; padding: 0 10px; }.stage-toolbar svg { height: 14px; width: 14px; } -.runtime-browser { height: calc(100% - 54px); min-height: 600px; }.browser-bar { height: 38px; }.browser-bar > strong { font-size: 12px; min-width: 180px; }.browser-bar > b { font-size: 12px; }.runtime-browser iframe { height: calc(100% - 38px); } +.runtime-stage { background: #eef2f8; overflow: hidden; padding: 10px 12px 12px; }.stage-toolbar { height: 44px; margin-bottom: 10px; }.stage-toolbar button { font-size: 12px; min-height: 44px; padding: 0 10px; }.stage-toolbar svg { height: 14px; width: 14px; } +.runtime-canvas-scroll { height: calc(100% - 54px); min-height: 600px; }.runtime-browser { height: 100%; min-height: 600px; }.browser-bar { height: 38px; }.browser-bar > strong { font-size: 12px; min-width: 180px; }.browser-bar > b { font-size: 12px; }.runtime-browser iframe { height: calc(100% - 38px); } .assistant-panel { overflow: hidden; }.assistant-panel > header,.chat-context,.quick-prompts,.chat-composer,.assistant-foot { flex-shrink: 0; }.assistant-panel > header { min-height: 72px; padding: 14px 16px; }.assistant-panel > header strong { font-size: 15px; }.assistant-panel > header small { font-size: 12px; }.director-avatar { height: 40px; width: 40px; } .chat-context { font-size: 12px; padding: 10px 14px; }.conversation { padding: 18px 16px; }.assistant-guide { border-radius: 14px; gap: 9px; padding: 15px; }.assistant-guide > strong { font-size: 14px; }.assistant-guide > p { font-size: 12px; }.assistant-guide > span { grid-template-columns: 28px 1fr; padding: 9px; }.assistant-guide > span b { font-size: 12px; }.assistant-guide > span small { font-size: 12px; } .conversation article > span { font-size: 12px; }.conversation article > p { border-radius: 5px 14px 14px 14px; font-size: 13px; line-height: 1.55; padding: 12px 13px; }.proposal-card > header { padding: 12px; }.proposal-card > header strong { font-size: 12px; }.proposal-card li { font-size: 12px; }.proposal-card > div button { font-size: 12px; min-height: 36px; }.quick-prompts { padding: 10px 12px 6px; }.quick-prompts button { font-size: 12px; min-height: 32px; padding: 6px 10px; }.chat-composer { border-radius: 14px; margin: 8px 12px 10px; }.chat-composer textarea { font-size: 13px; min-height: 92px; padding: 13px; }.chat-composer footer span { font-size: 12px; }.assistant-foot { font-size: 12px; padding-bottom: 11px; } diff --git a/components/agent-eval-dashboard.module.css b/components/agent-eval-dashboard.module.css index f3e6b1c..92ed02f 100644 --- a/components/agent-eval-dashboard.module.css +++ b/components/agent-eval-dashboard.module.css @@ -368,10 +368,10 @@ align-items: flex-start; gap: 8px; padding: 10px 11px; - border: 1px solid #f0d4d8; + border: 1px solid #d8e2f0; border-radius: 11px; - color: #8b3642; - background: #fff7f8; + color: #53657e; + background: #f7faff; font-size: 12px; line-height: 1.45; } @@ -379,7 +379,7 @@ .gateList li svg { width: 17px; flex: 0 0 auto; - color: #c83d4d; + color: #316cff; } .gateList .gateClear { diff --git a/components/agent-eval-dashboard.tsx b/components/agent-eval-dashboard.tsx index 11b079a..1a783b7 100644 --- a/components/agent-eval-dashboard.tsx +++ b/components/agent-eval-dashboard.tsx @@ -72,10 +72,16 @@ function displayDate(value: string): string { : "—"; } -export function AgentEvalDashboard() { +export function AgentEvalDashboard({ + initialPlatform, +}: { + initialPlatform: AgentV3PlatformEvidence; +}) { const [secret, setSecret] = useState(""); const [summary, setSummary] = useState(null); - const [platform, setPlatform] = useState(null); + const [platform, setPlatform] = useState( + initialPlatform, + ); const [busy, setBusy] = useState<"load" | "run" | null>(null); const [error, setError] = useState(null); @@ -97,7 +103,7 @@ export function AgentEvalDashboard() { const loadSummary = useCallback(async () => { if (!secret.trim()) { - setError("Enter the configured internal access secret."); + setError("Enter the operator access secret to load private run evidence."); return; } setBusy("load"); @@ -108,12 +114,12 @@ export function AgentEvalDashboard() { setPlatform(payload.platform ?? null); } catch (requestError) { setSummary(null); - setPlatform(null); + setPlatform(initialPlatform); setError(requestError instanceof Error ? requestError.message : "Internal evaluation evidence is unavailable."); } finally { setBusy(null); } - }, [request, secret]); + }, [initialPlatform, request, secret]); const submit = useCallback((event: FormEvent) => { event.preventDefault(); @@ -188,9 +194,7 @@ export function AgentEvalDashboard() { { label: "Average latency", value: duration(summary.averageLatencyMs), detail: `recorded model estimate ${cost(summary.estimatedModelCostUsd)}`, icon: Clock3 }, ] : [], [summary]); const gateBlockers = platform?.dataGate.blockers ?? [ - "Load private evidence to evaluate the immutable V2 baseline.", - "Authorized live model matrix evidence is not loaded.", - "Failure-cluster and Design Agent reports are not loaded.", + "Private candidate evidence is not loaded.", ]; return ( @@ -207,9 +211,9 @@ export function AgentEvalDashboard() {

One truthful view of the deterministic core, model runs, repair evidence, design verification, and the data gate protecting production defaults.

@@ -236,7 +240,7 @@ export function AgentEvalDashboard() {

-

The access value stays in page memory and is sent only to the internal API. Traces exclude source, credentials, and private reasoning.

+

This optional operator view controls promotion of a future agent candidate only. It never blocks Studio builds, previews or publishing. The access value stays in page memory and is sent only to the internal API.

@@ -247,7 +251,7 @@ export function AgentEvalDashboard() { autoComplete="off" value={secret} onChange={(event) => setSecret(event.target.value)} - placeholder="Configured server-side" + placeholder="Enter operator secret" /> @@ -288,7 +326,7 @@ export function DropsBotWebhookConnection({ {message ?

{message}

: null} - {evidence ? ( + {accessVerified && evidence ? (
@@ -319,7 +357,7 @@ export function DropsBotWebhookConnection({
) : null} - {events.length ? ( + {accessVerified && events.length ? (
Recent sanitized events {events.slice(0, 5).map((event) => ( diff --git a/components/platform/integration-catalog.tsx b/components/platform/integration-catalog.tsx index 266d446..e9001cf 100644 --- a/components/platform/integration-catalog.tsx +++ b/components/platform/integration-catalog.tsx @@ -1,19 +1,22 @@ "use client"; import { - Bot, Braces, - BrainCircuit, - CloudUpload, DatabaseZap, - GitBranch, KeyRound, - Network, - Route, Send, Sparkles, } from "lucide-react"; +import Image from "next/image"; import Link from "next/link"; +import { + SiAnthropic, + SiGithub, + SiMoonshotai, + SiOpenrouter, + SiVercel, +} from "react-icons/si"; +import { TbBrandOpenai } from "react-icons/tb"; import { useMemo, useSyncExternalStore } from "react"; import { Button } from "@/components/ui/button"; @@ -22,15 +25,15 @@ import { StatusBadge, type PlatformStatus } from "./platform-ui"; const integrations = [ { id: "free", name: "Free Auto", eyebrow: "Deterministic fallback", description: "Built-in planning and generation fallback. No provider key is required.", icon: Sparkles, href: "/", fixed: "built-in" }, - { id: "dropstab", name: "DropsTab API", eyebrow: "Market intelligence", description: "Session-only key for documented market, unlock, funding, and activity capabilities.", icon: DatabaseZap, href: "/?connections=1&provider=dropstab" }, - { id: "dropsbot", name: "Drops Bot + Telegram", eyebrow: "Monitoring and delivery", description: "Guided account, channel, bot, and provider verification. No delivery is assumed.", icon: Send, href: "/?connections=1&provider=dropsbot&flow=telegram-channel", fixed: "setup" }, - { id: "openai", name: "OpenAI", eyebrow: "Bring your key", description: "Use a session-only OpenAI API project key and a provider-returned model.", icon: BrainCircuit, href: "/?connections=1&provider=openai" }, - { id: "anthropic", name: "Anthropic", eyebrow: "Bring your key", description: "Use a session-only Anthropic API key for supported Claude models.", icon: Bot, href: "/?connections=1&provider=anthropic" }, - { id: "openrouter", name: "OpenRouter", eyebrow: "Account and BYOK", description: "PKCE member flow or session-only key with provider-returned model catalog.", icon: Route, href: "/?connections=1&provider=openrouter" }, - { id: "kimi", name: "Kimi", eyebrow: "Long context", description: "Use a session-only Moonshot API key and verified model identifier.", icon: Network, href: "/?connections=1&provider=kimi" }, + { id: "dropstab", name: "DropsTab API", eyebrow: "Market intelligence", description: "Session-only key for documented market, unlock, funding, and activity capabilities.", icon: DatabaseZap, logo: "/brand/dropstab-mark.svg", href: "/?connections=1&provider=dropstab" }, + { id: "dropsbot", name: "Drops Bot + Telegram", eyebrow: "Monitoring and delivery", description: "Guided account, channel, bot, and provider verification. No delivery is assumed.", icon: Send, logo: "/brand/drops-bot-avatar.png", href: "/?connections=1&provider=dropsbot&flow=telegram-channel", fixed: "setup" }, + { id: "openai", name: "OpenAI", eyebrow: "Bring your key", description: "Use a session-only OpenAI API project key and a provider-returned model.", icon: TbBrandOpenai, href: "/?connections=1&provider=openai" }, + { id: "anthropic", name: "Anthropic", eyebrow: "Bring your key", description: "Use a session-only Anthropic API key for supported Claude models.", icon: SiAnthropic, href: "/?connections=1&provider=anthropic" }, + { id: "openrouter", name: "OpenRouter", eyebrow: "Account and BYOK", description: "PKCE member flow or session-only key with provider-returned model catalog.", icon: SiOpenrouter, href: "/?connections=1&provider=openrouter" }, + { id: "kimi", name: "Kimi", eyebrow: "Long context", description: "Use a session-only Moonshot API key and verified model identifier.", icon: SiMoonshotai, href: "/?connections=1&provider=kimi" }, { id: "custom", name: "Custom provider", eyebrow: "OpenAI-compatible", description: "Configure a public HTTPS chat-completions endpoint for this browser tab.", icon: Braces, href: "/?connections=1&provider=custom" }, - { id: "github", name: "GitHub App", eyebrow: "Repository delivery", description: "Project-scoped branch, commit, and pull request actions require configured credentials and approval.", icon: GitBranch, href: "/projects", fixed: "setup" }, - { id: "vercel", name: "Vercel deployment", eyebrow: "Preview and release", description: "A deployment is shown ready only after Vercel returns a confirmed provider receipt.", icon: CloudUpload, href: "/projects", fixed: "setup" }, + { id: "github", name: "GitHub App", eyebrow: "Repository delivery", description: "Project-scoped branch, commit, and pull request actions require configured credentials and approval.", icon: SiGithub, href: "/projects", fixed: "setup" }, + { id: "vercel", name: "Vercel deployment", eyebrow: "Preview and release", description: "A deployment is shown ready only after Vercel returns a confirmed provider receipt.", icon: SiVercel, href: "/projects", fixed: "setup" }, ] as const; function subscribeToSessionConnections(onStoreChange: () => void) { @@ -66,7 +69,7 @@ export function IntegrationCatalog() { const label = fixed === "built-in" ? "Built in" : sessionConfigured ? "Session configured" : "Setup required"; return (
-
{label}
+
{"logo" in integration ? {`${integration.name} : {label}

{integration.eyebrow}

{integration.name}

{integration.description}

diff --git a/components/platform/platform-shell.tsx b/components/platform/platform-shell.tsx index d02b44d..0d141ab 100644 --- a/components/platform/platform-shell.tsx +++ b/components/platform/platform-shell.tsx @@ -1,11 +1,11 @@ -import Image from "next/image"; import Link from "next/link"; import type { ReactNode } from "react"; -import { ArrowRight, Plus, ShieldCheck } from "lucide-react"; +import { Plus, ShieldCheck } from "lucide-react"; import "@/app/styles/platform-tailwind.css"; import { Button } from "@/components/ui/button"; +import { DropsBrand } from "@/components/drops-brand"; const navigation = [ { href: "/projects", label: "Projects" }, @@ -29,10 +29,7 @@ export function PlatformShell({
- - - - Drops Studio +
diff --git a/components/preview-canvas.tsx b/components/preview-canvas.tsx index 33719c4..be0f391 100644 --- a/components/preview-canvas.tsx +++ b/components/preview-canvas.tsx @@ -12,7 +12,6 @@ import { Database, Eye, ExternalLink, - FileCode2, Folder, Flame, Link2, @@ -288,11 +287,11 @@ export function PreviewCanvas({ preset, spec, values, market, dataMode, predicti : "Plan growth loop"; return ( -
-
- {spec ? "Product plan" : preset.shortTitle} · Concept preview - {dataMode === "live" ? "Live DropsTab data" : "Sample data"} -
+
@@ -384,7 +383,7 @@ export function PreviewCanvas({ preset, spec, values, market, dataMode, predicti

{spec @@ -414,16 +413,10 @@ export function PreviewCanvas({ preset, spec, values, market, dataMode, predicti

-
- - {dataMode === "live" ? "DropsTab receipt verified" : "Sample data is labelled"} - Sandbox starts after Build -
{preset.preview === "channel" ? ( ) : null} -

Concept preview only. Real data, delivery and public state are labelled separately.

); } diff --git a/components/project-studio.tsx b/components/project-studio.tsx index bb7929e..ae6b1a4 100644 --- a/components/project-studio.tsx +++ b/components/project-studio.tsx @@ -29,6 +29,7 @@ import { Layers3, LoaderCircle, Monitor, + Minus, MousePointer2, Palette, Play, @@ -120,6 +121,9 @@ import { ProjectV2SyncError, saveProjectV2ToCloud, } from "@/lib/project-v2-sync-client"; +import { canonicalProjectV2Json } from "@/lib/project-v2-hash"; +import { refreshLegacyProjectV2Migration } from "@/lib/project-v2-migration"; +import { refreshGeneratedProjectV2Template } from "@/lib/project-template-materializer"; import { validateEditableRuntimeHtml } from "@/lib/source-workspace"; import { addWorkspaceFile, @@ -521,6 +525,57 @@ type PendingSpecCommit = { spec: GeneratedProjectSpec; }; +const PROJECT_V2_BUILD_TASKS = ["typecheck", "lint", "test", "build"] as const; + +function projectV2BuildEvidence(projectV2?: ProjectV2): { + passed: number; + total: number; + verified: boolean; +} { + if (!projectV2) return { passed: 0, total: 5, verified: false }; + let passed = 0; + for (const taskId of PROJECT_V2_BUILD_TASKS) { + let latest: ProjectV2["runs"][number] | undefined; + for (let index = projectV2.runs.length - 1; index >= 0; index -= 1) { + const candidate = projectV2.runs[index]; + if ( + candidate.taskId === taskId + && candidate.projectRevision === projectV2.revision + ) { + latest = candidate; + break; + } + } + if (latest?.status === "succeeded") passed += 1; + } + if ( + projectV2.preview?.status === "ready" + && projectV2.preview.projectRevision === projectV2.revision + && projectV2.preview.url + ) { + passed += 1; + } + return { passed, total: 5, verified: passed === 5 }; +} + +function currentProjectV2PreviewUrl(projectV2?: ProjectV2): string | null { + if ( + !projectV2?.preview?.url + || projectV2.preview.status !== "ready" + || projectV2.preview.projectRevision !== projectV2.revision + ) { + return null; + } + try { + const url = new URL(projectV2.preview.url); + return url.protocol === "https:" && !url.username && !url.password + ? url.toString() + : null; + } catch { + return null; + } +} + export function ProjectStudio() { const params = useParams<{ id: string }>(); const iframeRef = useRef(null); @@ -543,6 +598,7 @@ export function ProjectStudio() { const [loaded, setLoaded] = useState(false); const [tab, setTab] = useState("project"); const [device, setDevice] = useState("desktop"); + const [canvasZoom, setCanvasZoom] = useState(100); const [designMode, setDesignMode] = useState(false); const [selectedBlock, setSelectedBlock] = useState( null, @@ -595,8 +651,46 @@ export function ProjectStudio() { expectedUpdatedAt: string | null, ): Promise => { const save = async () => { + let candidate = next; + if ( + next.projectV2 + && canonicalProjectV2Json(next.projectV2.productSpec) + !== canonicalProjectV2Json(next.spec) + ) { + try { + const refreshed = next.projectV2.manifest.framework.name === "legacy-html" + ? await refreshLegacyProjectV2Migration({ + project: next.projectV2, + generatedProject: next, + }) + : await refreshGeneratedProjectV2Template({ + project: next.projectV2, + spec: next.spec, + }); + candidate = { ...next, projectV2: refreshed }; + const current = projectRef.current; + if ( + current?.id === next.id + && current.updatedAt === next.updatedAt + && current.projectV2?.revision === next.projectV2.revision + && canonicalProjectV2Json(current.spec) + === canonicalProjectV2Json(next.spec) + ) { + projectRef.current = candidate; + committedProjectRef.current = candidate; + setProject(candidate); + setDirty(true); + } + } catch { + setProjectSyncStatus("error"); + setToast( + "Generated Project V2 files could not be refreshed. Your previous revision is still safe.", + ); + return false; + } + } try { - const result = await saveProjectSafely(next, { expectedUpdatedAt }); + const result = await saveProjectSafely(candidate, { expectedUpdatedAt }); if (result.status === "conflict") { setProjectSyncStatus("conflict"); setToast( @@ -613,10 +707,10 @@ export function ProjectStudio() { } if (!cloudSyncAvailableRef.current) { - if (next.projectV2 && projectV2SyncAvailableRef.current) { + if (candidate.projectV2 && projectV2SyncAvailableRef.current) { try { const v2Record = await saveProjectV2ToCloud( - next.projectV2, + candidate.projectV2, projectV2CloudRevisionRef.current ?? 0, ); projectV2CloudRevisionRef.current = v2Record.storageRevision; @@ -645,13 +739,13 @@ export function ProjectStudio() { setProjectSyncStatus("saving"); try { const record = await saveMemberProjectToCloud( - next, + candidate, cloudRevisionRef.current ?? 0, ); cloudRevisionRef.current = record.revision; - if (next.projectV2) { + if (candidate.projectV2) { const v2Record = await saveProjectV2ToCloud( - next.projectV2, + candidate.projectV2, projectV2CloudRevisionRef.current ?? 0, ); projectV2CloudRevisionRef.current = v2Record.storageRevision; @@ -843,6 +937,24 @@ export function ProjectStudio() { migrateGeneratedProjectToV2(projectV2Source), ); } + if ( + canonicalProjectV2Json(projectV2.productSpec) + !== canonicalProjectV2Json(spec) + ) { + try { + projectV2 = projectV2.manifest.framework.name === "legacy-html" + ? await refreshLegacyProjectV2Migration({ + project: projectV2, + generatedProject: projectV2Source, + }) + : await refreshGeneratedProjectV2Template({ + project: projectV2, + spec, + }); + } catch { + setToast("Project V2 source refresh will retry when you save"); + } + } const migrated: GeneratedProject = { ...found, spec, @@ -1196,6 +1308,10 @@ export function ProjectStudio() { () => secureEditableRuntimeSrcDoc(runtimeHtml), [runtimeHtml], ); + const runtimePreviewUrl = useMemo( + () => currentProjectV2PreviewUrl(project?.projectV2), + [project?.projectV2], + ); const trustedRuntimeSmoke = (runtimeProject?.quality?.runtimeSmoke?.mode === "server-artifact" || runtimeProject?.quality?.runtimeSmoke?.mode === "server-inspection") @@ -1214,9 +1330,12 @@ export function ProjectStudio() { [hostDataProvider, runtimeProject, runtimeSmoke, trustedRuntimeSmoke], ); const browserTelemetryReady = Boolean( - runtimeSmoke?.mode === "browser" + runtimePreviewUrl + || ( + runtimeSmoke?.mode === "browser" && runtimeSmoke.executed - && runtimeSmoke.runtime, + && runtimeSmoke.runtime + ), ); const activeProvider = useMemo(() => { if (!project) return "free" as ProjectProvider; @@ -1645,7 +1764,11 @@ export function ProjectStudio() { }; projectRef.current = conversationDraft; setProject(conversationDraft); - if (activeProject.projectV2 && projectV2SyncAvailableRef.current) { + if ( + activeProject.projectV2 + && projectV2SyncAvailableRef.current + && activeProvider !== "free" + ) { try { await fetch("/api/access", { credentials: "same-origin", @@ -1662,10 +1785,9 @@ export function ProjectStudio() { accept: "application/json", "content-type": "application/json", }; - const key = - provider === "free" || provider === "gateway" - ? null - : window.sessionStorage.getItem(`drops-studio:${provider}`); + const key = provider === "gateway" + ? null + : window.sessionStorage.getItem(`drops-studio:${provider}`); if (provider === "openrouter" && key) { headers["x-openrouter-key"] = key; } else if (key) { @@ -1812,7 +1934,27 @@ export function ProjectStudio() { provider === "free" ? null : window.sessionStorage.getItem(`drops-studio:${provider}`); - if (provider === "custom" && key) { + if (provider === "free") { + const deterministic = selectedBlock?.kind === "element" + ? createFreeElementDirectorProposal( + activeProject.spec, + instruction, + selectedBlock, + ) + : createFreeDirectorProposal( + activeProject.spec, + instruction, + selectedBlock?.id, + ); + proposal = { + ...deterministic, + label: "Free Director deterministic change set", + summary: [ + ...deterministic.summary, + "Applying this proposal refreshes the generated Project V2 files; run Builder afterward for a verified Sandbox preview.", + ], + }; + } else if (provider === "custom" && key) { const endpoint = window.sessionStorage.getItem( "drops-studio:custom-endpoint", ); @@ -1933,10 +2075,7 @@ export function ProjectStudio() { method: "POST", headers, body: JSON.stringify({ - provider: - provider === "free" || provider === "gateway" - ? undefined - : provider, + provider: provider === "gateway" ? undefined : provider, model, guestId: window.sessionStorage.getItem("drops-studio:guest-id"), prompt: `Revise the existing product without changing its category (${activeProject.spec.presetId}).\nUser change: ${instruction}\nSelected canvas item: ${JSON.stringify(selectedBlock ?? { kind: "product", label: "whole product" })}.\nIf the selected item kind is element, use its exact id in elementEdit and return only the requested focused style/copy change there while preserving the rest of the product.\nCurrent product: ${JSON.stringify({ name: activeProject.spec.name, tagline: activeProject.spec.tagline, description: activeProject.spec.description, tools: activeProject.spec.tools })}\nCurrent blueprint: ${JSON.stringify(activeProject.spec.blueprint)}\nCurrent design: ${JSON.stringify({ theme: activeProject.spec.theme, design: activeProject.spec.design, experience: activeProject.spec.experience, gameDirection: activeProject.spec.gameDirection, elements: activeProject.spec.elements })}`, @@ -2188,6 +2327,13 @@ export function ProjectStudio() { const currentProject = commitPendingSpec() ?? projectRef.current ?? project; if (!currentProject) return; + const sandboxPreviewUrl = currentProjectV2PreviewUrl( + currentProject.projectV2, + ); + if (sandboxPreviewUrl) { + window.open(sandboxPreviewUrl, "_blank", "noopener,noreferrer"); + return; + } if (currentProject.publishedUrl && !dirty) window.open( currentProject.publishedUrl, @@ -2950,6 +3096,11 @@ export function ProjectStudio() { const legacyPublication = published && !managedPublication; const quality = qualityReport ?? evaluateProjectQuality(project.spec, project.html); + const builderEvidence = projectV2BuildEvidence(project.projectV2); + const hasProjectV2 = Boolean(project.projectV2); + const releaseEvidenceReady = hasProjectV2 + ? builderEvidence.verified + : quality.readyToPublish; const reality = getProductReality(project.spec.presetId); const externalSetup = quality.launchStatus === "external-setup-required"; const researchOnly = quality.launchStatus === "research-only"; @@ -3035,10 +3186,18 @@ export function ProjectStudio() { ? "Research app published" : "Web app published" : dirty - ? "Edits pending" + ? builderEvidence.verified + ? "Verified draft" + : hasProjectV2 + ? "Build pending" + : "Edits pending" : externalSetup ? "Needs connection" - : "Draft"} + : hasProjectV2 + ? builderEvidence.verified + ? "Verified draft" + : "Build pending" + : "Draft"}
@@ -3201,10 +3360,22 @@ export function ProjectStudio() { Foundation DropsTab × guided Drops Bot setup - + 4 Ship - {quality.score}/100 quality + + {builderEvidence.verified + ? `${builderEvidence.passed}/${builderEvidence.total} builder checks` + : hasProjectV2 + ? `${builderEvidence.passed}/${builderEvidence.total} checks · run Builder` + : `${quality.score}/100 legacy publish quality`} +
- {quality.readyToPublish + {builderEvidence.verified + ? "Project V2 build verified" + : hasProjectV2 + ? "Project V2 build pending" + : quality.readyToPublish ? releaseLabel : "Build needs attention"} - {externalSetup + {builderEvidence.verified + ? "Typecheck, lint, tests, production build and live Sandbox preview passed for this file revision. The legacy score below applies only to standalone /p publishing." + : hasProjectV2 + ? `${builderEvidence.passed}/${builderEvidence.total} current-revision checks have verified evidence. Open Builder to run the remaining checks and start the live preview.` + : externalSetup ? "The web setup app can publish, but the external outcome is not live until it is connected and verified." : "Deterministic checks run on every edit and before every publish."} @@ -4505,6 +4696,29 @@ export function ProjectStudio() {
+ + + + +
-
-
+
+
+
+
@@ -4552,20 +4781,25 @@ export function ProjectStudio() { : "Loading preview"} +
+