Compare commits

...

32 Commits

Author SHA1 Message Date
Claude
7a2435067c fix(frontend): add missing data-testid='agent-title' to NewAgentLibraryView
- Add testId prop to Breadcrumbs component interface
- Pass agent-title test ID to agent name breadcrumb in SelectedViewLayout
- Fixes marketplace-agent.spec.ts test that expects agent-title element in library page
- Test was failing because NewAgentLibraryView didn't have the test ID that exists in OldAgentLibraryView
2025-12-19 19:02:49 +00:00
Claude
519d4bd67e Merge branch 'feat/publish-newer-agent-marketplace' into claude/fix-marketplace-test-aaIIq 2025-12-19 18:59:02 +00:00
Zamil Majdy
4350ad95d7 fix(frontend): fix infinite loop in PublishAgentModal and simplify useThumbnailImages
- Changed updateState to setCurrentState in usePublishAgentModal to prevent circular dependency
- Simplified useThumbnailImages by using JSON.stringify for stable dependency tracking instead of manual refs
- Removed duplicate helpers.ts file from MainAgentPage
- All changes preserve functionality while reducing code complexity

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:53:42 +01:00
Zamil Majdy
8769104242 style(frontend): remove unnecessary !important text color overrides
- Remove !important from text-neutral-500 classes where component styling is sufficient
- Fix AgentSelectStep: 2 instances of !text-neutral-500 → text-neutral-500
- Fix AgentReviewStep: 2 instances of !text-neutral-500 → text-neutral-500
- Total: 4 unnecessary CSS overrides removed for cleaner styling
- Components now rely on natural text color inheritance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:18:09 +01:00
Zamil Majdy
22c486583a fix(frontend): replace all 'any' types with proper pre-generated TypeScript types
- Replace PublishState.submissionData: any → StoreSubmission | null
- Replace publishedSubmissionData?: any → StoreSubmission | null in all function signatures
- Replace MyAgent and StoreSubmission parameter types throughout components
- Fix array method parameter types (map, filter, sort, reduce) with proper types
- Remove redundant text-xs class when variant="small" is already set
- Total: ~20 'any' types replaced with safer pre-generated TypeScript types

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:16:00 +01:00
Zamil Majdy
87d4bcd025 fix(frontend): replace remaining status !== 200 checks with okData() helper
- Replace 2 status !== 200 checks in useMarketplaceUpdate.ts with okData()
- Replace 2 status !== 200 checks in usePublishAgentModal.ts with okData()
- Total: 3 additional status checks replaced (now 9 total)
- All manual status checks now use safer okData() pattern

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:08:48 +01:00
Zamil Majdy
c2c7cc94cf fix(frontend): replace status === 200 checks with okData() helper in marketplace files
- Replace 4 manual status checks in useAgentSelectStep.ts with okData() helper
- Replace 1 manual status check in useMarketplaceUpdate.ts with okData() helper
- Replace 1 manual status check in AgentVersionChangelog.tsx with okData() helper
- Total: 6 status === 200 checks replaced with safer okData() pattern
- Add proper TypeScript typing with StoreAgentDetails type
- Maintain backward compatibility while improving code safety

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:04:29 +01:00
Zamil Majdy
e6eef0379e feat(frontend): implement real changelog and fix marketplace agent display
- Fix AgentInfo frontend crash by using okData() helper for safe data access
- Implement real changelog functionality with include_changelog API parameter
- Add ChangelogEntry backend model and proper API typing
- Fix version sorting to use Math.max instead of array indexing
- Replace manual status checks with okData() helper throughout
- Create reusable marketplace helper functions in src/components/contextual
- Fix TypeScript errors by replacing 'any' types with proper generated types
- Improve accessibility by making agent cards properly clickable buttons
- Remove unnecessary useMemo and simplify complex helper functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 17:56:03 +01:00
Zamil Majdy
cc2c183a5f feat(frontend): polish changelog Read more button to match design specs
- Move chevron to left side of text matching reference design
- Update to proper 16x16 icon size with refined stroke styling
- Replace inline styles with proper Tailwind classes
- Use design system colors (text-neutral-900) instead of manual styling
- Add hover states and dark mode support
- Maintain incremental loading (3 versions at a time)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 11:53:58 +01:00
Zamil Majdy
73fcadf0e8 Merge branch 'dev' into feat/publish-newer-agent-marketplace 2025-12-19 11:35:26 +01:00
Zamil Majdy
44d17885ed fix(frontend): fix infinite loop in thumbnail images hook
- Use refs to track previous prop values and prevent infinite re-renders
- Only update images state when initialImages actually changes
- Fix maximum update depth exceeded error in PublishAgentModal
- Ensure thumbnail images load correctly on first modal open

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 11:31:49 +01:00
Zamil Majdy
21652a5422 feat(frontend): fix marketplace banner layout and placement
- Fix banner layout from horizontal to vertical using flex-col
- Move banner to correct breadcrumb location inside SelectedViewLayout
- Remove duplicate breadcrumb from top level in NewAgentLibraryView
- Add marketplace publish functionality to builder actions
- Create unified MarketplaceBanners component for consistency
- Fix apostrophe escaping in banner text for ESLint compliance
- Remove unused isSaving property from PublishToMarketplace hook

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 10:59:56 +01:00
Bently
eed07b173a fix(frontend/builder): automatically frame agent when opening in builder (#11640)
## Summary
- Fixed auto-frame timing in new builder - now calls `fitView` after
nodes are rendered instead of on mount
- Replaced manual viewport calculation in legacy builder with React
Flow's `fitView` for consistency
- Both builders now properly center and frame all blocks when opening an
agent

  ## Test plan
- [x] Open an existing agent with multiple blocks in the new builder -
verify all blocks are visible and centered
- [x] Open an existing agent in the legacy builder - verify all blocks
are visible and centered
  - [x] Verify the manual "Frame" button still works correctly
2025-12-18 18:07:40 +00:00
Ubbe
c5e8b0b08f fix(frontend): modal hidden overflow (#11642)
## Changes 🏗️

### Before

<img width="300" height="528" alt="Screenshot 2025-12-18 at 19 07 37"
src="https://github.com/user-attachments/assets/6bedf02d-e1fd-4f87-956e-96fcd9996392"
/>


### After

<img width="300" height="576" alt="Screenshot 2025-12-18 at 19 02 12"
src="https://github.com/user-attachments/assets/f3175b94-447c-41b0-83cf-4334c02d1378"
/>


## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run locally and check crop
2025-12-18 19:51:24 +01:00
Ubbe
cd3e35df9e fix(frontend): small library/mobile improvements (#11626)
## Changes 🏗️

Adds the following improvements:

### Prevent credential row overflowing on mobile 📱 

**Before**

<img width="300" height="469" alt="Screenshot 2025-12-15 at 16 42 05"
src="https://github.com/user-attachments/assets/0d27394c-cec9-45a4-be82-804827343212"
/>

**After**

<img width="300" height="446" alt="Screenshot 2025-12-15 at 16 44 22"
src="https://github.com/user-attachments/assets/0f19e220-500d-4488-955e-612d38704727"
/>

_Just hide the ****** on mobile..._

### Make touch targets bigger on 📱 on the mobile menu

**Before**

<img width="300" height="607" alt="Screenshot 2025-12-15 at 16 58 28"
src="https://github.com/user-attachments/assets/762b7d4e-5269-41a4-88d2-ea745c50324e"
/>

Touch targets were quite small on mobile, especially for people with big
fingers...

**After**

<img width="300" height="589" alt="Screenshot 2025-12-15 at 16 54 02"
src="https://github.com/user-attachments/assets/beede0c4-5439-47a9-8bec-143b44306c6b"
/>

### New `<OverflowText />` component

<img width="600" height="551" alt="Screenshot 2025-12-15 at 16 48 20"
src="https://github.com/user-attachments/assets/a969223f-dd6a-497a-857e-18483aea28d7"
/>

A component that will render text like `<Text />`, but automatically
displays `...` and the full text content on a tooltip if it detects
there is no space for the full text length. Pretty useful for the type
of dashboard we are building, where sometimes titles or user-generated
content can be quite long, making the UI look whack.

### Google Drive Picker

Only allow the removal of files if it is not in read-only mode.


## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Checkout branch locally
  - [x] Test the above
2025-12-18 19:33:30 +01:00
Ubbe
4a7bc006a8 hotfix(frontend): chat should be disabled by default (#11639)
### Changes 🏗️

Chat should be disabled by default; otherwise, it flashes, and if Launch
Darkly fails to fail, it is dangerous.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run locally with Launch Darkly disabled and test the above
2025-12-18 19:04:13 +01:00
dependabot[bot]
4c474417bc chore(frontend/deps-dev): bump import-in-the-middle from 1.14.2 to 2.0.0 in /autogpt_platform/frontend (#11357)
Bumps
[import-in-the-middle](https://github.com/nodejs/import-in-the-middle)
from 1.14.2 to 2.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/import-in-the-middle/releases">import-in-the-middle's
releases</a>.</em></p>
<blockquote>
<h2>import-in-the-middle: v2.0.0</h2>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.15.0...import-in-the-middle-v2.0.0">2.0.0</a>
(2025-10-14)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<p>This was only a new major out of an abundance of caution. The hook
code has been converted to ESM to work around some loader issues. There
should actually be no breaking changes when using
<code>import-in-the-middle/hook.mjs</code> or the exported
<code>Hook</code> API.</p>
<h3>Features</h3>
<ul>
<li>convert all modules running in loader thread to ESM (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/210">#210</a>)
(<a
href="da7c7a6904">da7c7a6</a>)</li>
</ul>
<h2>import-in-the-middle: v1.15.0</h2>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.4...import-in-the-middle-v1.15.0">1.15.0</a>
(2025-10-09)</h2>
<h3>Features</h3>
<ul>
<li>Compatibility with specifier imports (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/211">#211</a>)
(<a
href="83d662a8e1">83d662a</a>)</li>
</ul>
<h2>import-in-the-middle: v1.14.4</h2>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.3...import-in-the-middle-v1.14.4">1.14.4</a>
(2025-09-25)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Revert &quot;use <code>createRequire</code> to load
<code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)&quot;
(<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/208">#208</a>)
(<a
href="f23b7ef9e8">f23b7ef</a>)</li>
</ul>
<h2>import-in-the-middle: v1.14.3</h2>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.2...import-in-the-middle-v1.14.3">1.14.3</a>
(2025-09-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>use <code>createRequire</code> to load <code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)
(<a
href="81a2ae0ea0">81a2ae0</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodejs/import-in-the-middle/blob/main/CHANGELOG.md">import-in-the-middle's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.15.0...import-in-the-middle-v2.0.0">2.0.0</a>
(2025-10-14)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<p>Converting all modules running in the loader thread to ESM should not
be a
breaking change for most users since it primarily affects internal
implementation
details. However, if you were referencing internal CJS files like
<code>hook.js</code> this will no longer work.</p>
<h3>Features</h3>
<ul>
<li>convert all modules running in loader thread to ESM (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/210">#210</a>)
(<a
href="da7c7a6904">da7c7a6</a>)</li>
</ul>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.4...import-in-the-middle-v1.15.0">1.15.0</a>
(2025-10-09)</h2>
<h3>Features</h3>
<ul>
<li>Compatibility with specifier imports (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/211">#211</a>)
(<a
href="83d662a8e1">83d662a</a>)</li>
</ul>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.3...import-in-the-middle-v1.14.4">1.14.4</a>
(2025-09-25)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Revert &quot;use <code>createRequire</code> to load
<code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)&quot;
(<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/208">#208</a>)
(<a
href="f23b7ef9e8">f23b7ef</a>)</li>
</ul>
<h2><a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.2...import-in-the-middle-v1.14.3">1.14.3</a>
(2025-09-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>use <code>createRequire</code> to load <code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)
(<a
href="81a2ae0ea0">81a2ae0</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d96619f7f2"><code>d96619f</code></a>
chore: release v2.0.0 (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/214">#214</a>)</li>
<li><a
href="da7c7a6904"><code>da7c7a6</code></a>
feat!: convert all modules running in loader thread to ESM (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/210">#210</a>)</li>
<li><a
href="b8fd97e98c"><code>b8fd97e</code></a>
chore: npm ignore several files used for dev (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/137">#137</a>)</li>
<li><a
href="94837a7427"><code>94837a7</code></a>
chore: release v1.15.0 (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/213">#213</a>)</li>
<li><a
href="83d662a8e1"><code>83d662a</code></a>
feat: Compatibility with specifier imports (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/211">#211</a>)</li>
<li><a
href="ffb5682e3f"><code>ffb5682</code></a>
chore: release v1.14.4 (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/209">#209</a>)</li>
<li><a
href="f23b7ef9e8"><code>f23b7ef</code></a>
fix: Revert &quot;use <code>createRequire</code> to load
<code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)&quot;
(<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/208">#208</a>)</li>
<li><a
href="f67ecb8323"><code>f67ecb8</code></a>
chore: release v1.14.3 (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/206">#206</a>)</li>
<li><a
href="81a2ae0ea0"><code>81a2ae0</code></a>
fix: use <code>createRequire</code> to load <code>hook.js</code> (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/205">#205</a>)</li>
<li><a
href="2fb1f21b3d"><code>2fb1f21</code></a>
chore: use npm@11 for OIDC publishing (<a
href="https://redirect.github.com/nodejs/import-in-the-middle/issues/204">#204</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.14.2...import-in-the-middle-v2.0.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for import-in-the-middle since your current
version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=import-in-the-middle&package-manager=npm_and_yarn&previous-version=1.14.2&new-version=2.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Ubbe <hi@ubbe.dev>
2025-12-18 18:58:27 +01:00
dependabot[bot]
99e2261254 chore(frontend/deps-dev): bump eslint-config-next from 15.5.2 to 15.5.6 in /autogpt_platform/frontend (#11355)
Bumps
[eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next)
from 15.5.2 to 15.5.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">eslint-config-next's
releases</a>.</em></p>
<blockquote>
<h2>v15.5.6</h2>
<blockquote>
<p>[!NOTE]<br />
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>Turbopack: don't define process.cwd() in node_modules <a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83452">#83452</a></li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/mischnic"><code>@​mischnic</code></a> for
helping!</p>
<h2>v15.5.5</h2>
<blockquote>
<p>[!NOTE]<br />
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>Split code-frame into separate compiled package (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84238">#84238</a>)</li>
<li>Add deprecation warning to Runtime config (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84650">#84650</a>)</li>
<li>fix: unstable_cache should perform blocking revalidation during ISR
revalidation (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84716">#84716</a>)</li>
<li>feat: <code>experimental.middlewareClientMaxBodySize</code> body
cloning limit (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84722">#84722</a>)</li>
<li>fix: missing next/link types with typedRoutes (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84779">#84779</a>)</li>
</ul>
<h3>Misc Changes</h3>
<ul>
<li>docs: early October improvements and fixes (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84334">#84334</a>)</li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/devjiwonchoi"><code>@​devjiwonchoi</code></a>,
<a href="https://github.com/ztanner"><code>@​ztanner</code></a>, and <a
href="https://github.com/icyJoseph"><code>@​icyJoseph</code></a> for
helping!</p>
<h2>v15.5.4</h2>
<blockquote>
<p>[!NOTE]<br />
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>fix: ensure onRequestError is invoked when otel enabled (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83343">#83343</a>)</li>
<li>fix: devtools initial position should be from next config (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83571">#83571</a>)</li>
<li>[devtool] fix overlay styles are missing (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83721">#83721</a>)</li>
<li>Turbopack: don't match dynamic pattern for node_modules packages (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83176">#83176</a>)</li>
<li>Turbopack: don't treat metadata routes as RSC (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/82911">#82911</a>)</li>
<li>[turbopack] Improve handling of symlink resolution errors in
track_glob and read_glob (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83357">#83357</a>)</li>
<li>Turbopack: throw large static metadata error earlier (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/82939">#82939</a>)</li>
<li>fix: error overlay not closing when backdrop clicked (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83981">#83981</a>)</li>
<li>Turbopack: flush Node.js worker IPC on error (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/84077">#84077</a>)</li>
</ul>
<h3>Misc Changes</h3>
<ul>
<li>[CNA] use linter preference (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83194">#83194</a>)</li>
<li>CI: use KV for test timing data (<a
href="https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next/issues/83745">#83745</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="55ef0e3ebc"><code>55ef0e3</code></a>
v15.5.6</li>
<li><a
href="81f530db26"><code>81f530d</code></a>
v15.5.5</li>
<li><a
href="40f1d7814d"><code>40f1d78</code></a>
v15.5.4</li>
<li><a
href="07d1cbc9c6"><code>07d1cbc</code></a>
v15.5.3</li>
<li>See full diff in <a
href="https://github.com/vercel/next.js/commits/v15.5.6/packages/eslint-config-next">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint-config-next&package-manager=npm_and_yarn&previous-version=15.5.2&new-version=15.5.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Ubbe <hi@ubbe.dev>
2025-12-18 18:55:57 +01:00
dependabot[bot]
cab498fa8c chore(deps): Bump actions/stale from 9 to 10 (#10871)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/releases">actions/stale's
releases</a>.</em></p>
<blockquote>
<h2>v10.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade to node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1279">actions/stale#1279</a>
Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></li>
</ul>
<h3>Enhancement</h3>
<ul>
<li>Introducing sort-by option by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1254">actions/stale#1254</a></li>
</ul>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade actions/publish-immutable-action from 0.0.3 to 0.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1186">actions/stale#1186</a></li>
<li>Upgrade undici from 5.28.4 to 5.28.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1201">actions/stale#1201</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.0 to 4.0.2 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1226">actions/stale#1226</a></li>
<li>Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1233">actions/stale#1233</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/stale/pull/1251">actions/stale#1251</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/stale/pull/1277">actions/stale#1277</a></li>
</ul>
<h3>Documentation changes</h3>
<ul>
<li>Changelog update for recent releases by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1224">actions/stale#1224</a></li>
<li>Permissions update in Readme by <a
href="https://github.com/ghadimir"><code>@​ghadimir</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1248">actions/stale#1248</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1224">actions/stale#1224</a></li>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1248">actions/stale#1248</a></li>
<li><a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1277">actions/stale#1277</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1279">actions/stale#1279</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v9...v10.0.0">https://github.com/actions/stale/compare/v9...v10.0.0</a></p>
<h2>v9.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Documentation update by <a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
<li>Update undici from 5.28.2 to 5.28.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1150">actions/stale#1150</a></li>
<li>Update actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1091">actions/stale#1091</a></li>
<li>Update actions/publish-action from 0.2.2 to 0.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1147">actions/stale#1147</a></li>
<li>Update ts-jest from 29.1.1 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1175">actions/stale#1175</a></li>
<li>Update <code>@​actions/core</code> from 1.10.1 to 1.11.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1191">actions/stale#1191</a></li>
<li>Update <code>@​types/jest</code> from 29.5.11 to 29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1193">actions/stale#1193</a></li>
<li>Update <code>@​actions/cache</code> from 3.2.2 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1194">actions/stale#1194</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/stale/compare/v9...v9.1.0">https://github.com/actions/stale/compare/v9...v9.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/stale/blob/main/CHANGELOG.md">actions/stale's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h1>[9.1.0]</h1>
<h2>What's Changed</h2>
<ul>
<li>Documentation update by <a
href="https://github.com/Marukome0743"><code>@​Marukome0743</code></a>
in <a
href="https://redirect.github.com/actions/stale/pull/1116">actions/stale#1116</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1179">actions/stale#1179</a></li>
<li>Update undici from 5.28.2 to 5.28.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1150">actions/stale#1150</a></li>
<li>Update actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1091">actions/stale#1091</a></li>
<li>Update actions/publish-action from 0.2.2 to 0.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1147">actions/stale#1147</a></li>
<li>Update ts-jest from 29.1.1 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1175">actions/stale#1175</a></li>
<li>Update <code>@​actions/core</code> from 1.10.1 to 1.11.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1191">actions/stale#1191</a></li>
<li>Update <code>@​types/jest</code> from 29.5.11 to 29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1193">actions/stale#1193</a></li>
<li>Update <code>@​actions/cache</code> from 3.2.2 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1194">actions/stale#1194</a></li>
</ul>
<h1>[9.0.0]</h1>
<h2>Breaking Changes</h2>
<ol>
<li>Action is now stateful: If the action ends because of <a
href="https://github.com/actions/stale#operations-per-run">operations-per-run</a>
then the next run will start from the first unprocessed issue skipping
the issues processed during the previous run(s). The state is reset when
all the issues are processed. This should be considered for scheduling
workflow runs.</li>
<li>Version 9 of this action updated the runtime to Node.js 20. All
scripts are now run with Node.js 20 instead of Node.js 16 and are
affected by any breaking changes between Node.js 16 and 20.</li>
</ol>
<h2>What Else Changed</h2>
<ol>
<li>Performance optimization that removes unnecessary API calls by <a
href="https://github.com/dsame"><code>@​dsame</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1033/">#1033</a>;
fixes <a
href="https://redirect.github.com/actions/stale/issues/792">#792</a></li>
<li>Logs displaying current GitHub API rate limit by <a
href="https://github.com/dsame"><code>@​dsame</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/1032">#1032</a>;
addresses <a
href="https://redirect.github.com/actions/stale/issues/1029">#1029</a></li>
</ol>
<p>For more information, please read the <a
href="https://github.com/actions/stale#readme">action documentation</a>
and its <a href="https://github.com/actions/stale#statefulness">section
about statefulness</a></p>
<h1>[4.1.1]</h1>
<p>In scope of this release we updated <a
href="https://redirect.github.com/actions/stale/pull/957">actions/core
to 1.10.0</a> for v4 and <a
href="https://redirect.github.com/actions/stale/pull/662">fixed issues
operation count</a>.</p>
<h1>[8.0.0]</h1>
<p>⚠️ This version contains breaking changes ⚠️</p>
<ul>
<li>New option labels-to-remove-when-stale enables users to specify list
of comma delimited labels that will be removed when the issue or PR
becomes stale by <a
href="https://github.com/panticmilos"><code>@​panticmilos</code></a> <a
href="https://redirect.github.com/actions/stale/issues/770">actions/stale#770</a></li>
<li>Skip deleting the branch in the upstream of a forked repo by <a
href="https://github.com/dsame"><code>@​dsame</code></a> <a
href="https://redirect.github.com/actions/stale/pull/913">actions/stale#913</a></li>
<li>abort the build on the error by <a
href="https://github.com/dsame"><code>@​dsame</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/935">actions/stale#935</a></li>
</ul>
<h1>[7.0.0]</h1>
<p>⚠️ Breaking change ⚠️</p>
<ul>
<li>Allow daysBeforeStale options to be float by <a
href="https://github.com/irega"><code>@​irega</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/841">actions/stale#841</a></li>
<li>Use cache in check-dist.yml by <a
href="https://github.com/jongwooo"><code>@​jongwooo</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/876">actions/stale#876</a></li>
<li>fix print outputs step in existing workflows by <a
href="https://github.com/irega"><code>@​irega</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/859">actions/stale#859</a></li>
<li>Update issue and PR templates, add/delete workflow files by <a
href="https://github.com/IvanZosimov"><code>@​IvanZosimov</code></a> in
<a
href="https://redirect.github.com/actions/stale/pull/880">actions/stale#880</a></li>
<li>Update how stale handles exempt items by <a
href="https://github.com/johnsudol"><code>@​johnsudol</code></a> in <a
href="https://redirect.github.com/actions/stale/pull/874">actions/stale#874</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3a9db7e6a4"><code>3a9db7e</code></a>
Upgrade to node 24 (<a
href="https://redirect.github.com/actions/stale/issues/1279">#1279</a>)</li>
<li><a
href="8f717f0dfc"><code>8f717f0</code></a>
Bumps form-data (<a
href="https://redirect.github.com/actions/stale/issues/1277">#1277</a>)</li>
<li><a
href="a92fd57ffe"><code>a92fd57</code></a>
build(deps): bump undici from 5.28.5 to 5.29.0 (<a
href="https://redirect.github.com/actions/stale/issues/1251">#1251</a>)</li>
<li><a
href="128b2c81d0"><code>128b2c8</code></a>
Introducing sort-by option (<a
href="https://redirect.github.com/actions/stale/issues/1254">#1254</a>)</li>
<li><a
href="f78de9780e"><code>f78de97</code></a>
Update README.md (<a
href="https://redirect.github.com/actions/stale/issues/1248">#1248</a>)</li>
<li><a
href="816d9db1ab"><code>816d9db</code></a>
Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 (<a
href="https://redirect.github.com/actions/stale/issues/1233">#1233</a>)</li>
<li><a
href="ba23c1cb02"><code>ba23c1c</code></a>
upgrade actions/cache from 4.0.0 to 4.0.2 (<a
href="https://redirect.github.com/actions/stale/issues/1226">#1226</a>)</li>
<li><a
href="a65e88a9b9"><code>a65e88a</code></a>
build(deps): bump undici from 5.28.4 to 5.28.5 (<a
href="https://redirect.github.com/actions/stale/issues/1201">#1201</a>)</li>
<li><a
href="d4df79c591"><code>d4df79c</code></a>
Updates to CHANGELOG.MD for recent releases (<a
href="https://redirect.github.com/actions/stale/issues/1224">#1224</a>)</li>
<li><a
href="ee7ef89499"><code>ee7ef89</code></a>
build(deps): bump actions/publish-immutable-action from 0.0.3 to 0.0.4
(<a
href="https://redirect.github.com/actions/stale/issues/1186">#1186</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/stale/compare/v9...v10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/stale&package-manager=github_actions&previous-version=9&new-version=10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Update the stale-issues workflow to use `actions/stale@v10` instead of
`v9`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
747d4ea73a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
2025-12-18 17:34:04 +00:00
Bently
22078671df feat(frontend): increase file upload size limit to 256MB (#11634)
- Updated Next.js configuration to set body size limits for server
actions and API routes.
- Enhanced error handling in the API client to provide user-friendly
messages for file size errors.
- Added user-friendly error messages for 413 Payload Too Large responses
in API error parsing.

These changes ensure that file uploads are consistent with backend
limits and improve user experience during uploads.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] Upload a file bigger than 10MB and it works
- [X] Upload a file bigger than 256MB and you see a official error
stating the max file size is 256MB
2025-12-18 17:29:20 +00:00
dependabot[bot]
0082a72657 chore(deps): Bump actions/labeler from 5 to 6 (#10868)
Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/labeler/releases">actions/labeler's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/jcambass"><code>@​jcambass</code></a> in <a
href="https://redirect.github.com/actions/labeler/pull/802">actions/labeler#802</a></li>
</ul>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade Node.js version to 24 in action and dependencies <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/labeler/pull/891">actions/labeler#891</a>
Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></li>
</ul>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade eslint-config-prettier from 9.0.0 to 9.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/711">actions/labeler#711</a></li>
<li>Upgrade eslint from 8.52.0 to 8.55.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/720">actions/labeler#720</a></li>
<li>Upgrade <code>@​types/jest</code> from 29.5.6 to 29.5.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/719">actions/labeler#719</a></li>
<li>Upgrade <code>@​types/js-yaml</code> from 4.0.8 to 4.0.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/718">actions/labeler#718</a></li>
<li>Upgrade <code>@​typescript-eslint/parser</code> from 6.9.0 to 6.14.0
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/717">actions/labeler#717</a></li>
<li>Upgrade prettier from 3.0.3 to 3.1.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/726">actions/labeler#726</a></li>
<li>Upgrade eslint from 8.55.0 to 8.56.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/725">actions/labeler#725</a></li>
<li>Upgrade <code>@​typescript-eslint/parser</code> from 6.14.0 to
6.19.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/745">actions/labeler#745</a></li>
<li>Upgrade eslint-plugin-jest from 27.4.3 to 27.6.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/744">actions/labeler#744</a></li>
<li>Upgrade <code>@​typescript-eslint/eslint-plugin</code> from 6.9.0 to
6.20.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/750">actions/labeler#750</a></li>
<li>Upgrade prettier from 3.1.1 to 3.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/752">actions/labeler#752</a></li>
<li>Upgrade undici from 5.26.5 to 5.28.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/757">actions/labeler#757</a></li>
<li>Upgrade braces from 3.0.2 to 3.0.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/789">actions/labeler#789</a></li>
<li>Upgrade minimatch from 9.0.3 to 10.0.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/805">actions/labeler#805</a></li>
<li>Upgrade <code>@​actions/core</code> from 1.10.1 to 1.11.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/811">actions/labeler#811</a></li>
<li>Upgrade typescript from 5.4.3 to 5.7.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/819">actions/labeler#819</a></li>
<li>Upgrade <code>@​typescript-eslint/parser</code> from 7.3.1 to 8.17.0
by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/824">actions/labeler#824</a></li>
<li>Upgrade prettier from 3.2.5 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/825">actions/labeler#825</a></li>
<li>Upgrade <code>@​types/jest</code> from 29.5.12 to 29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/827">actions/labeler#827</a></li>
<li>Upgrade eslint-plugin-jest from 27.9.0 to 28.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/832">actions/labeler#832</a></li>
<li>Upgrade ts-jest from 29.1.2 to 29.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/831">actions/labeler#831</a></li>
<li>Upgrade <code>@​vercel/ncc</code> from 0.38.1 to 0.38.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/830">actions/labeler#830</a></li>
<li>Upgrade typescript from 5.7.2 to 5.7.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/835">actions/labeler#835</a></li>
<li>Upgrade eslint-plugin-jest from 28.9.0 to 28.11.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/839">actions/labeler#839</a></li>
<li>Upgrade undici from 5.28.4 to 5.28.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/842">actions/labeler#842</a></li>
<li>Upgrade <code>@​octokit/request-error</code> from 5.0.1 to 5.1.1 by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/labeler/pull/846">actions/labeler#846</a></li>
</ul>
<h3>Documentation changes</h3>
<ul>
<li>Add note regarding <code>pull_request_target</code> to README.md by
<a href="https://github.com/silverwind"><code>@​silverwind</code></a> in
<a
href="https://redirect.github.com/actions/labeler/pull/669">actions/labeler#669</a></li>
<li>Update readme with additional examples and important note about
<code>pull_request_target</code> event by <a
href="https://github.com/IvanZosimov"><code>@​IvanZosimov</code></a> in
<a
href="https://redirect.github.com/actions/labeler/pull/721">actions/labeler#721</a></li>
<li>Document update - permission section by <a
href="https://github.com/harithavattikuti"><code>@​harithavattikuti</code></a>
in <a
href="https://redirect.github.com/actions/labeler/pull/840">actions/labeler#840</a></li>
<li>Improvement in documentation for pull_request_target event usage in
README by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/labeler/pull/871">actions/labeler#871</a></li>
<li>Fix broken links in documentation by <a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
in <a
href="https://redirect.github.com/actions/labeler/pull/822">actions/labeler#822</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/silverwind"><code>@​silverwind</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/labeler/pull/669">actions/labeler#669</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/labeler/pull/802">actions/labeler#802</a></li>
<li><a
href="https://github.com/suyashgaonkar"><code>@​suyashgaonkar</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/labeler/pull/822">actions/labeler#822</a></li>
<li><a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/labeler/pull/840">actions/labeler#840</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/labeler/pull/891">actions/labeler#891</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="634933edcd"><code>634933e</code></a>
publish-action upgrade to 0.4.0 from 0.2.2 (<a
href="https://redirect.github.com/actions/labeler/issues/901">#901</a>)</li>
<li><a
href="f1a63e87db"><code>f1a63e8</code></a>
Update Node.js version to 24 in action and dependencies (<a
href="https://redirect.github.com/actions/labeler/issues/891">#891</a>)</li>
<li><a
href="b0a1180683"><code>b0a1180</code></a>
Bump <code>@​octokit/request-error</code> from 5.0.1 to 5.1.1 (<a
href="https://redirect.github.com/actions/labeler/issues/846">#846</a>)</li>
<li><a
href="110d44140c"><code>110d441</code></a>
Update README.md (<a
href="https://redirect.github.com/actions/labeler/issues/871">#871</a>)</li>
<li><a
href="bee50fefe1"><code>bee50fe</code></a>
Bump undici from 5.28.4 to 5.28.5 (<a
href="https://redirect.github.com/actions/labeler/issues/842">#842</a>)</li>
<li><a
href="6463cdb00e"><code>6463cdb</code></a>
Bump eslint-plugin-jest from 28.9.0 to 28.11.0 (<a
href="https://redirect.github.com/actions/labeler/issues/839">#839</a>)</li>
<li><a
href="c209686724"><code>c209686</code></a>
Bump typescript from 5.7.2 to 5.7.3 (<a
href="https://redirect.github.com/actions/labeler/issues/835">#835</a>)</li>
<li><a
href="5184940b54"><code>5184940</code></a>
Bump <code>@​vercel/ncc</code> from 0.38.1 to 0.38.3 (<a
href="https://redirect.github.com/actions/labeler/issues/830">#830</a>)</li>
<li><a
href="3629d5568b"><code>3629d55</code></a>
Document update - permission section (<a
href="https://redirect.github.com/actions/labeler/issues/840">#840</a>)</li>
<li><a
href="d24f7f3731"><code>d24f7f3</code></a>
Bump ts-jest from 29.1.2 to 29.2.5 (<a
href="https://redirect.github.com/actions/labeler/issues/831">#831</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/labeler/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/labeler&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 17:16:22 +00:00
Ubbe
9a1d940677 fix(frontend): onboarding run card (#11636)
## Changes 🏗️

### Before

<img width="300" height="730" alt="Screenshot 2025-12-18 at 17 16 57"
src="https://github.com/user-attachments/assets/f57efc83-aa55-4371-96af-e294cab9b03a"
/>

- extra label
- overflow

### After

<img width="300" height="642" alt="Screenshot 2025-12-18 at 17 41 53"
src="https://github.com/user-attachments/assets/50721293-c67c-438a-9c9d-ff7ffae11d14"
/>

## Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run locally
  - [x] Test the above
2025-12-18 18:08:19 +01:00
Zamil Majdy
1a8ed4c291 fix(frontend): simplify marketplace version history to clean list
Replace card-based version display with simple list format:
- Single line per version: version number + current badge + description
- Current version highlighted in blue text
- No borders or background styling on items
- Clean, scannable layout with minimal visual noise
- Fixed TypeScript issues with proper data type checking

This provides the same information in a much cleaner, less
overwhelming format that's easier to read at a glance.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:51:10 +01:00
Zamil Majdy
c5acb0d4cc feat(frontend): add inline version changelog to marketplace agent pages
Add version history display directly to marketplace agent pages showing
all available versions with visual indicators for the current version.

This provides users with immediate visibility into agent version history
without needing to click a separate button, similar to the library page
changelog functionality but rendered inline.

Features:
- Fetches agentGraphVersions from store API
- Displays versions in descending order (newest first)
- Highlights current version with blue styling and 'Current' badge
- Shows 'Published marketplace version' description for each version
- Gracefully falls back to basic version display if data unavailable
- Maintains existing 'Last updated' information

This enhances marketplace discoverability by making version information
immediately visible to users browsing agent details.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:48:08 +01:00
Zamil Majdy
fc0d0903f2 fix(frontend): correct submission version field name
Fix pending submission detection by using 'agent_version' field instead
of 'version' to match the StoreSubmission interface. This ensures
creators don't see duplicate publish update banners when they have
pending submissions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:41:20 +01:00
Abhimanyu Yadav
e640d36265 feat(frontend): Add special handling for AGENT block type in form and output handlers (#11595)
<!-- Clearly explain the need for these changes: -->

Agent blocks require different handling compared to standard blocks,
particularly for:
- Handle ID generation (using direct keys instead of generated IDs)
- Form data storage structure (nested under `inputs` key)
- Field ID parsing (filtering out schema path prefixes)

This PR implements special handling for `BlockUIType.AGENT` throughout
the form rendering and output handling components to ensure agents work
correctly in the flow editor.

### Changes 🏗️

<!-- Concisely describe all of the changes made in this pull request:
-->

- **CustomNode.tsx**: Pass `uiType` prop to `OutputHandler` component
- **FormCreator.tsx**: 
- Store agent form data in `hardcodedValues.inputs` instead of directly
in `hardcodedValues`
- Extract initial values from `hardcodedValues.inputs` for agent blocks
- **OutputHandler.tsx**: 
  - Accept `uiType` prop
- Use direct key as handle ID for agents instead of
`generateHandleId(key)`
- **useMarketplaceAgentsContent.ts**: 
- Fetch full agent details using `getV2GetLibraryAgent` before adding to
builder
- Ensures agent schemas are properly populated (fixes issue where
marketplace endpoint returns empty schemas)
- **AnyOfField.tsx**: Generate handle IDs for agents by filtering out
"root" and "properties" from schema path
- **FieldTemplate.tsx**: Apply same handle ID generation logic for agent
fields

### Checklist 📋

#### For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Add an agent block from marketplace and verify it renders
correctly
- [x] Connect inputs/outputs to/from an agent block and verify
connections work
- [x] Fill in form fields for an agent block and verify data persists
correctly
  - [x] Verify agent blocks work in both new and existing flows
- [x] Test that non-agent blocks still work as before (regression test)
2025-12-18 03:42:55 +00:00
Zamil Majdy
ca53b752d2 feat(platform): add marketplace update notification banner
Add marketplace update notification banner that shows when newer agent versions
are available on the marketplace. This enables non-creator users to see and
update to newer agent versions.

Key features:
- Update banner appears when marketplace has newer agent version
- Version History modal shows all available versions
- Direct update to latest version with explicit graph_version parameter
- Support for both creator and non-creator users
- Added database fields for graph versions and agent graph ID

Frontend changes:
- New useMarketplaceUpdate hook for marketplace version detection
- AgentVersionChangelog component for version history display
- Switch from submissions API to store API for non-creator access
- Clean update mechanism using graph_version parameter

Backend changes:
- Added agentGraphVersions and agentGraphId fields to StoreAgent view
- Updated LibraryAgentUpdateRequest model with graph_version field
- Enhanced store endpoints to expose graph version data
- Fixed all test cases to include new required fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 21:22:03 +01:00
Zamil Majdy
1d3d60f7ef feat(frontend): add marketplace update notification banner
Add notification banner when user's local agent version is newer than published version.
Allow direct publishing of agent updates via banner button.
Skip agent selection step and pre-populate form when publishing updates.
Fix database view to use correct agent graph version instead of store listing version.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 19:32:00 +01:00
Zamil Majdy
cc9179178f feat(block): Human in The Loop Block restructure (#11627)
## Summary

This PR refactors the Human-In-The-Loop (HITL) review system backend to
improve data handling and API consistency.

## Changes

### Backend Refactoring

#### 1. **Block Output Schema Update** (`human_in_the_loop.py`)
- Replaced single `reviewed_data` and `status` fields with separate
`approved_data` and `rejected_data` outputs
- This allows downstream blocks to handle approved vs rejected data
differently without checking status
- Simplified test outputs to match new schema

#### 2. **Review Data Handling** (`human_review.py`)
- Modified `get_or_create_human_review` to always return
`review.payload` regardless of approval status
- Previously returned `None` for rejected reviews, which could cause
data loss
- Now preserves reviewer-modified data for both approved and rejected
cases

#### 3. **API Route Simplification** (`review/routes.py`)
- Streamlined review decision processing logic using ternary operator
- Unified data handling for both approved and rejected reviews
- Maintains backward compatibility while improving code clarity

## Why These Changes?

- **Better Data Flow**: Separate output pins for approved/rejected data
make workflow design more intuitive
- **Data Preservation**: Rejected reviews can still pass modified data
downstream for logging or alternative processing
- **Cleaner API**: Simplified decision processing reduces code
complexity and potential bugs

## Testing

- All existing tests pass with updated schema
- Backward compatibility maintained for existing workflows
- Human review functionality verified in both approved and rejected
scenarios

## Related

This is the backend portion of changes from #11529, applied separately
to the `feat/hitl` branch.
2025-12-16 12:14:14 +00:00
Ubbe
e8d37ab116 feat(frontend): add nice scrollable tabs on Selected Run view (#11596)
## Changes 🏗️


https://github.com/user-attachments/assets/7e49ed5b-c818-4aa3-b5d6-4fa86fada7ee

When the content of Summary + Outputs + Inputs is long enough, it will
show in this new `<ScrollableTabs />` component, which auto-scrolls the
content as you click on a tab.

## Checklist 📋

### For code changes

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally
  - [x] Check the new page with scrollable tabs
2025-12-15 16:57:36 +07:00
Ubbe
7f7ef6a271 feat(frontend): imporve agent inputs read-only (#11621)
## Changes 🏗️

The main goal of this PR is to improve how we display inputs used for a
given task.

Agent inputs can be of many types (text, long text, date, select, file,
etc.). Until now, we have tried to display them as text, which has not
always worked. Given we already have `<RunAgentInputs />`, which uses
form elements to display the inputs ( _prefilled with data_ ), most of
the time it will look better and less buggy than text.

### Before

<img width="800" height="614" alt="Screenshot 2025-12-14 at 17 45 44"
src="https://github.com/user-attachments/assets/3d851adf-9638-46c1-adfa-b5e68dc78bb0"
/>

### After

<img width="800" height="708" alt="Screenshot 2025-12-14 at 17 45 21"
src="https://github.com/user-attachments/assets/367f32b4-2c30-4368-8d63-4cad06e32437"
/>

### Other improvements

- 🗑️  Removed `<EditInputsModal />`
- it is not used given the API does not support editing inputs for a
schedule yt
- Made `<InformationTooltip />` icon size customisable    

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app locally
- [x] Check the new view tasks use the form elements instead of text to
display inputs
2025-12-15 00:11:27 +07:00
Ubbe
aefac541d9 fix(frontend): force light mode for now (#11619)
## Changes 🏗️

We have the setup for light/dark mode support ( Tailwind + `next-themes`
), but not the capacity yet from contributions to make the app dark-mode
ready. First, we need to make it look good in light mode 😆

This disables `dark:` mode classes on the code, to prevent the app
looking oopsie when the user is seeing it with a browser with dark mode
preference:

### Before these changes

<img width="800" height="739" alt="Screenshot 2025-12-14 at 17 09 25"
src="https://github.com/user-attachments/assets/76333e03-930a-40b6-b91e-47ee01bf2c00"
/>

### After

<img width="800" height="722" alt="Screenshot 2025-12-14 at 16 55 46"
src="https://github.com/user-attachments/assets/34d85359-c68f-474c-8c66-2bebf28f923e"
/>

## Checklist 📋

### For code changes

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run the app on a browser with dark mode preference
  - [x] It still looks in light mode without broken styles
2025-12-15 00:10:36 +07:00
88 changed files with 3425 additions and 891 deletions

View File

@@ -11,7 +11,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v10
with: with:
# operations-per-run: 5000 # operations-per-run: 5000
stale-issue-message: > stale-issue-message: >

View File

@@ -61,6 +61,6 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@v5 - uses: actions/labeler@v6
with: with:
sync-labels: true sync-labels: true

View File

@@ -1,5 +1,5 @@
import logging import logging
from typing import Any, Literal from typing import Any
from prisma.enums import ReviewStatus from prisma.enums import ReviewStatus
@@ -45,11 +45,11 @@ class HumanInTheLoopBlock(Block):
) )
class Output(BlockSchemaOutput): class Output(BlockSchemaOutput):
reviewed_data: Any = SchemaField( approved_data: Any = SchemaField(
description="The data after human review (may be modified)" description="The data when approved (may be modified by reviewer)"
) )
status: Literal["approved", "rejected"] = SchemaField( rejected_data: Any = SchemaField(
description="Status of the review: 'approved' or 'rejected'" description="The data when rejected (may be modified by reviewer)"
) )
review_message: str = SchemaField( review_message: str = SchemaField(
description="Any message provided by the reviewer", default="" description="Any message provided by the reviewer", default=""
@@ -69,8 +69,7 @@ class HumanInTheLoopBlock(Block):
"editable": True, "editable": True,
}, },
test_output=[ test_output=[
("status", "approved"), ("approved_data", {"name": "John Doe", "age": 30}),
("reviewed_data", {"name": "John Doe", "age": 30}),
], ],
test_mock={ test_mock={
"get_or_create_human_review": lambda *_args, **_kwargs: ReviewResult( "get_or_create_human_review": lambda *_args, **_kwargs: ReviewResult(
@@ -116,8 +115,7 @@ class HumanInTheLoopBlock(Block):
logger.info( logger.info(
f"HITL block skipping review for node {node_exec_id} - safe mode disabled" f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
) )
yield "status", "approved" yield "approved_data", input_data.data
yield "reviewed_data", input_data.data
yield "review_message", "Auto-approved (safe mode disabled)" yield "review_message", "Auto-approved (safe mode disabled)"
return return
@@ -158,12 +156,11 @@ class HumanInTheLoopBlock(Block):
) )
if result.status == ReviewStatus.APPROVED: if result.status == ReviewStatus.APPROVED:
yield "status", "approved" yield "approved_data", result.data
yield "reviewed_data", result.data
if result.message: if result.message:
yield "review_message", result.message yield "review_message", result.message
elif result.status == ReviewStatus.REJECTED: elif result.status == ReviewStatus.REJECTED:
yield "status", "rejected" yield "rejected_data", result.data
if result.message: if result.message:
yield "review_message", result.message yield "review_message", result.message

View File

@@ -100,7 +100,7 @@ async def get_or_create_human_review(
return None return None
else: else:
return ReviewResult( return ReviewResult(
data=review.payload if review.status == ReviewStatus.APPROVED else None, data=review.payload,
status=review.status, status=review.status,
message=review.reviewMessage or "", message=review.reviewMessage or "",
processed=review.processed, processed=review.processed,

View File

@@ -442,6 +442,8 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
runs=agent.runs, runs=agent.runs,
rating=agent.rating, rating=agent.rating,
versions=agent.versions, versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at, last_updated=agent.updated_at,
) )
for agent in recommended_agents for agent in recommended_agents

View File

@@ -134,18 +134,14 @@ async def process_review_action(
# Build review decisions map # Build review decisions map
review_decisions = {} review_decisions = {}
for review in request.reviews: for review in request.reviews:
if review.approved: review_status = (
review_decisions[review.node_exec_id] = ( ReviewStatus.APPROVED if review.approved else ReviewStatus.REJECTED
ReviewStatus.APPROVED, )
review.reviewed_data, review_decisions[review.node_exec_id] = (
review.message, review_status,
) review.reviewed_data,
else: review.message,
review_decisions[review.node_exec_id] = ( )
ReviewStatus.REJECTED,
None,
review.message,
)
# Process all reviews # Process all reviews
updated_reviews = await process_all_reviews_for_execution( updated_reviews = await process_all_reviews_for_execution(

View File

@@ -538,6 +538,7 @@ async def update_library_agent(
library_agent_id: str, library_agent_id: str,
user_id: str, user_id: str,
auto_update_version: Optional[bool] = None, auto_update_version: Optional[bool] = None,
graph_version: Optional[int] = None,
is_favorite: Optional[bool] = None, is_favorite: Optional[bool] = None,
is_archived: Optional[bool] = None, is_archived: Optional[bool] = None,
is_deleted: Optional[Literal[False]] = None, is_deleted: Optional[Literal[False]] = None,
@@ -550,6 +551,7 @@ async def update_library_agent(
library_agent_id: The ID of the LibraryAgent to update. library_agent_id: The ID of the LibraryAgent to update.
user_id: The owner of this LibraryAgent. user_id: The owner of this LibraryAgent.
auto_update_version: Whether the agent should auto-update to active version. auto_update_version: Whether the agent should auto-update to active version.
graph_version: Specific graph version to update to.
is_favorite: Whether this agent is marked as a favorite. is_favorite: Whether this agent is marked as a favorite.
is_archived: Whether this agent is archived. is_archived: Whether this agent is archived.
settings: User-specific settings for this library agent. settings: User-specific settings for this library agent.
@@ -563,8 +565,8 @@ async def update_library_agent(
""" """
logger.debug( logger.debug(
f"Updating library agent {library_agent_id} for user {user_id} with " f"Updating library agent {library_agent_id} for user {user_id} with "
f"auto_update_version={auto_update_version}, is_favorite={is_favorite}, " f"auto_update_version={auto_update_version}, graph_version={graph_version}, "
f"is_archived={is_archived}, settings={settings}" f"is_favorite={is_favorite}, is_archived={is_archived}, settings={settings}"
) )
update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {} update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {}
if auto_update_version is not None: if auto_update_version is not None:
@@ -581,10 +583,23 @@ async def update_library_agent(
update_fields["isDeleted"] = is_deleted update_fields["isDeleted"] = is_deleted
if settings is not None: if settings is not None:
update_fields["settings"] = SafeJson(settings.model_dump()) update_fields["settings"] = SafeJson(settings.model_dump())
if not update_fields:
raise ValueError("No values were passed to update")
try: try:
# If graph_version is provided, update to that specific version
if graph_version is not None:
# Get the current agent to find its graph_id
agent = await get_library_agent(id=library_agent_id, user_id=user_id)
# Update to the specified version using existing function
return await update_agent_version_in_library(
user_id=user_id,
agent_graph_id=agent.graph_id,
agent_graph_version=graph_version,
)
# Otherwise, just update the simple fields
if not update_fields:
raise ValueError("No values were passed to update")
n_updated = await prisma.models.LibraryAgent.prisma().update_many( n_updated = await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id}, where={"id": library_agent_id, "userId": user_id},
data=update_fields, data=update_fields,

View File

@@ -385,6 +385,9 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel):
auto_update_version: Optional[bool] = pydantic.Field( auto_update_version: Optional[bool] = pydantic.Field(
default=None, description="Auto-update the agent version" default=None, description="Auto-update the agent version"
) )
graph_version: Optional[int] = pydantic.Field(
default=None, description="Specific graph version to update to"
)
is_favorite: Optional[bool] = pydantic.Field( is_favorite: Optional[bool] = pydantic.Field(
default=None, description="Mark the agent as a favorite" default=None, description="Mark the agent as a favorite"
) )

View File

@@ -284,6 +284,7 @@ async def update_library_agent(
library_agent_id=library_agent_id, library_agent_id=library_agent_id,
user_id=user_id, user_id=user_id,
auto_update_version=payload.auto_update_version, auto_update_version=payload.auto_update_version,
graph_version=payload.graph_version,
is_favorite=payload.is_favorite, is_favorite=payload.is_favorite,
is_archived=payload.is_archived, is_archived=payload.is_archived,
settings=payload.settings, settings=payload.settings,

View File

@@ -42,10 +42,12 @@ async def _get_cached_store_agents(
# Cache individual agent details for 15 minutes # Cache individual agent details for 15 minutes
@cached(maxsize=200, ttl_seconds=300, shared_cache=True) @cached(maxsize=200, ttl_seconds=300, shared_cache=True)
async def _get_cached_agent_details(username: str, agent_name: str): async def _get_cached_agent_details(
username: str, agent_name: str, include_changelog: bool = False
):
"""Cached helper to get agent details.""" """Cached helper to get agent details."""
return await backend.server.v2.store.db.get_store_agent_details( return await backend.server.v2.store.db.get_store_agent_details(
username=username, agent_name=agent_name username=username, agent_name=agent_name, include_changelog=include_changelog
) )

View File

@@ -256,7 +256,7 @@ async def log_search_term(search_query: str):
async def get_store_agent_details( async def get_store_agent_details(
username: str, agent_name: str username: str, agent_name: str, include_changelog: bool = False
) -> backend.server.v2.store.model.StoreAgentDetails: ) -> backend.server.v2.store.model.StoreAgentDetails:
"""Get PUBLIC store agent details from the StoreAgent view""" """Get PUBLIC store agent details from the StoreAgent view"""
logger.debug(f"Getting store agent details for {username}/{agent_name}") logger.debug(f"Getting store agent details for {username}/{agent_name}")
@@ -321,6 +321,27 @@ async def get_store_agent_details(
else: else:
recommended_schedule_cron = None recommended_schedule_cron = None
# Fetch changelog data if requested
changelog_data = None
if include_changelog and store_listing:
changelog_versions = (
await prisma.models.StoreListingVersion.prisma().find_many(
where={
"storeListingId": store_listing.id,
"submissionStatus": prisma.enums.SubmissionStatus.APPROVED,
},
order=[{"version": "desc"}],
)
)
changelog_data = [
backend.server.v2.store.model.ChangelogEntry(
version=str(version.version),
changes_summary=version.changesSummary or "No changes recorded",
date=version.createdAt,
)
for version in changelog_versions
]
logger.debug(f"Found agent details for {username}/{agent_name}") logger.debug(f"Found agent details for {username}/{agent_name}")
return backend.server.v2.store.model.StoreAgentDetails( return backend.server.v2.store.model.StoreAgentDetails(
store_listing_version_id=agent.storeListingVersionId, store_listing_version_id=agent.storeListingVersionId,
@@ -337,10 +358,13 @@ async def get_store_agent_details(
runs=agent.runs, runs=agent.runs,
rating=agent.rating, rating=agent.rating,
versions=agent.versions, versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at, last_updated=agent.updated_at,
active_version_id=active_version_id, active_version_id=active_version_id,
has_approved_version=has_approved_version, has_approved_version=has_approved_version,
recommended_schedule_cron=recommended_schedule_cron, recommended_schedule_cron=recommended_schedule_cron,
changelog=changelog_data,
) )
except backend.server.v2.store.exceptions.AgentNotFoundError: except backend.server.v2.store.exceptions.AgentNotFoundError:
raise raise
@@ -408,6 +432,8 @@ async def get_store_agent_by_version_id(
runs=agent.runs, runs=agent.runs,
rating=agent.rating, rating=agent.rating,
versions=agent.versions, versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at, last_updated=agent.updated_at,
) )
except backend.server.v2.store.exceptions.AgentNotFoundError: except backend.server.v2.store.exceptions.AgentNotFoundError:

View File

@@ -40,6 +40,8 @@ async def test_get_store_agents(mocker):
runs=10, runs=10,
rating=4.5, rating=4.5,
versions=["1.0"], versions=["1.0"],
agentGraphVersions=["1"],
agentGraphId="test-graph-id",
updated_at=datetime.now(), updated_at=datetime.now(),
is_available=False, is_available=False,
useForOnboarding=False, useForOnboarding=False,
@@ -83,6 +85,8 @@ async def test_get_store_agent_details(mocker):
runs=10, runs=10,
rating=4.5, rating=4.5,
versions=["1.0"], versions=["1.0"],
agentGraphVersions=["1"],
agentGraphId="test-graph-id",
updated_at=datetime.now(), updated_at=datetime.now(),
is_available=False, is_available=False,
useForOnboarding=False, useForOnboarding=False,
@@ -105,6 +109,8 @@ async def test_get_store_agent_details(mocker):
runs=15, runs=15,
rating=4.8, rating=4.8,
versions=["1.0", "2.0"], versions=["1.0", "2.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id-active",
updated_at=datetime.now(), updated_at=datetime.now(),
is_available=True, is_available=True,
useForOnboarding=False, useForOnboarding=False,

View File

@@ -7,6 +7,12 @@ import pydantic
from backend.util.models import Pagination from backend.util.models import Pagination
class ChangelogEntry(pydantic.BaseModel):
version: str
changes_summary: str
date: datetime.datetime
class MyAgent(pydantic.BaseModel): class MyAgent(pydantic.BaseModel):
agent_id: str agent_id: str
agent_version: int agent_version: int
@@ -55,12 +61,17 @@ class StoreAgentDetails(pydantic.BaseModel):
runs: int runs: int
rating: float rating: float
versions: list[str] versions: list[str]
agentGraphVersions: list[str]
agentGraphId: str
last_updated: datetime.datetime last_updated: datetime.datetime
recommended_schedule_cron: str | None = None recommended_schedule_cron: str | None = None
active_version_id: str | None = None active_version_id: str | None = None
has_approved_version: bool = False has_approved_version: bool = False
# Optional changelog data when include_changelog=True
changelog: list[ChangelogEntry] | None = None
class Creator(pydantic.BaseModel): class Creator(pydantic.BaseModel):
name: str name: str

View File

@@ -72,6 +72,8 @@ def test_store_agent_details():
runs=50, runs=50,
rating=4.5, rating=4.5,
versions=["1.0", "2.0"], versions=["1.0", "2.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id",
last_updated=datetime.datetime.now(), last_updated=datetime.datetime.now(),
) )
assert details.slug == "test-agent" assert details.slug == "test-agent"

View File

@@ -154,7 +154,11 @@ async def get_agents(
tags=["store", "public"], tags=["store", "public"],
response_model=backend.server.v2.store.model.StoreAgentDetails, response_model=backend.server.v2.store.model.StoreAgentDetails,
) )
async def get_agent(username: str, agent_name: str): async def get_agent(
username: str,
agent_name: str,
include_changelog: bool = fastapi.Query(default=False),
):
""" """
This is only used on the AgentDetails Page. This is only used on the AgentDetails Page.
@@ -164,7 +168,7 @@ async def get_agent(username: str, agent_name: str):
# URL decode the agent name since it comes from the URL path # URL decode the agent name since it comes from the URL path
agent_name = urllib.parse.unquote(agent_name).lower() agent_name = urllib.parse.unquote(agent_name).lower()
agent = await store_cache._get_cached_agent_details( agent = await store_cache._get_cached_agent_details(
username=username, agent_name=agent_name username=username, agent_name=agent_name, include_changelog=include_changelog
) )
return agent return agent

View File

@@ -388,6 +388,8 @@ def test_get_agent_details(
runs=100, runs=100,
rating=4.5, rating=4.5,
versions=["1.0.0", "1.1.0"], versions=["1.0.0", "1.1.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id",
last_updated=FIXED_NOW, last_updated=FIXED_NOW,
) )
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agent_details") mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agent_details")

View File

@@ -0,0 +1,45 @@
-- Fix StoreSubmission view to use agentGraphVersion instead of version for agent_version field
-- This ensures that submission.agent_version returns the actual agent graph version, not the store listing version number
BEGIN;
-- Recreate the view with the corrected agent_version field (using agentGraphVersion instead of version)
CREATE OR REPLACE VIEW "StoreSubmission" AS
SELECT
sl.id AS listing_id,
sl."owningUserId" AS user_id,
slv."agentGraphId" AS agent_id,
slv."agentGraphVersion" AS agent_version,
sl.slug,
COALESCE(slv.name, '') AS name,
slv."subHeading" AS sub_heading,
slv.description,
slv.instructions,
slv."imageUrls" AS image_urls,
slv."submittedAt" AS date_submitted,
slv."submissionStatus" AS status,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating,
slv.id AS store_listing_version_id,
slv."reviewerId" AS reviewer_id,
slv."reviewComments" AS review_comments,
slv."internalComments" AS internal_comments,
slv."reviewedAt" AS reviewed_at,
slv."changesSummary" AS changes_summary,
slv."videoUrl" AS video_url,
slv.categories
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count
FROM "AgentGraphExecution"
GROUP BY "AgentGraphExecution"."agentGraphId"
) ar ON ar."agentGraphId" = slv."agentGraphId"
WHERE sl."isDeleted" = false
GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv."agentGraphVersion", sl.slug, slv.name,
slv."subHeading", slv.description, slv.instructions, slv."imageUrls", slv."submittedAt",
slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments",
slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count;
COMMIT;

View File

@@ -0,0 +1,81 @@
-- Add agentGraphVersions field to StoreAgent view for consistent version comparison
-- This keeps the existing versions field unchanged and adds a new field with graph versions
-- This makes it safe for version comparison with LibraryAgent.graph_version
BEGIN;
-- Drop and recreate the StoreAgent view with new agentGraphVersions field
DROP VIEW IF EXISTS "StoreAgent";
CREATE OR REPLACE VIEW "StoreAgent" AS
WITH latest_versions AS (
SELECT
"storeListingId",
MAX(version) AS max_version
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
),
agent_versions AS (
SELECT
"storeListingId",
array_agg(DISTINCT version::text ORDER BY version::text) AS versions
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
),
agent_graph_versions AS (
SELECT
"storeListingId",
array_agg(DISTINCT "agentGraphVersion"::text ORDER BY "agentGraphVersion"::text) AS graph_versions
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
)
SELECT
sl.id AS listing_id,
slv.id AS "storeListingVersionId",
slv."createdAt" AS updated_at,
sl.slug,
COALESCE(slv.name, '') AS agent_name,
slv."videoUrl" AS agent_video,
slv."agentOutputDemoUrl" AS agent_output_demo,
COALESCE(slv."imageUrls", ARRAY[]::text[]) AS agent_image,
slv."isFeatured" AS featured,
p.username AS creator_username, -- Allow NULL for malformed sub-agents
p."avatarUrl" AS creator_avatar, -- Allow NULL for malformed sub-agents
slv."subHeading" AS sub_heading,
slv.description,
slv.categories,
slv.search,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(rs.avg_rating, 0.0)::double precision AS rating,
COALESCE(av.versions, ARRAY[slv.version::text]) AS versions,
COALESCE(agv.graph_versions, ARRAY[slv."agentGraphVersion"::text]) AS "agentGraphVersions",
slv."agentGraphId",
slv."isAvailable" AS is_available,
COALESCE(sl."useForOnboarding", false) AS "useForOnboarding"
FROM "StoreListing" sl
JOIN latest_versions lv
ON sl.id = lv."storeListingId"
JOIN "StoreListingVersion" slv
ON slv."storeListingId" = lv."storeListingId"
AND slv.version = lv.max_version
AND slv."submissionStatus" = 'APPROVED'
JOIN "AgentGraph" a
ON slv."agentGraphId" = a.id
AND slv."agentGraphVersion" = a.version
LEFT JOIN "Profile" p
ON sl."owningUserId" = p."userId"
LEFT JOIN "mv_review_stats" rs
ON sl.id = rs."storeListingId"
LEFT JOIN "mv_agent_run_counts" ar
ON a.id = ar."agentGraphId"
LEFT JOIN agent_versions av
ON sl.id = av."storeListingId"
LEFT JOIN agent_graph_versions agv
ON sl.id = agv."storeListingId"
WHERE sl."isDeleted" = false
AND sl."hasApprovedVersion" = true;
COMMIT;

View File

@@ -728,11 +728,13 @@ view StoreAgent {
description String description String
categories String[] categories String[]
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector")) search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
runs Int runs Int
rating Float rating Float
versions String[] versions String[]
is_available Boolean @default(true) agentGraphVersions String[]
useForOnboarding Boolean @default(false) agentGraphId String
is_available Boolean @default(true)
useForOnboarding Boolean @default(false)
// Materialized views used (refreshed every 15 minutes via pg_cron): // Materialized views used (refreshed every 15 minutes via pg_cron):
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId // - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId

View File

@@ -3,6 +3,14 @@ import { withSentryConfig } from "@sentry/nextjs";
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
experimental: {
serverActions: {
bodySizeLimit: "256mb",
},
// Increase body size limit for API routes (file uploads) - 256MB to match backend limit
proxyClientMaxBodySize: "256mb",
middlewareClientMaxBodySize: "256mb",
},
images: { images: {
domains: [ domains: [
// We dont need to maintain alphabetical order here // We dont need to maintain alphabetical order here

View File

@@ -137,9 +137,8 @@
"concurrently": "9.2.1", "concurrently": "9.2.1",
"cross-env": "10.1.0", "cross-env": "10.1.0",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-config-next": "15.5.2", "eslint-config-next": "15.5.7",
"eslint-plugin-storybook": "9.1.5", "eslint-plugin-storybook": "9.1.5",
"import-in-the-middle": "1.14.2",
"msw": "2.11.6", "msw": "2.11.6",
"msw-storybook-addon": "2.0.6", "msw-storybook-addon": "2.0.6",
"orval": "7.13.0", "orval": "7.13.0",

View File

@@ -331,14 +331,11 @@ importers:
specifier: 8.57.1 specifier: 8.57.1
version: 8.57.1 version: 8.57.1
eslint-config-next: eslint-config-next:
specifier: 15.5.2 specifier: 15.5.7
version: 15.5.2(eslint@8.57.1)(typescript@5.9.3) version: 15.5.7(eslint@8.57.1)(typescript@5.9.3)
eslint-plugin-storybook: eslint-plugin-storybook:
specifier: 9.1.5 specifier: 9.1.5
version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3) version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)
import-in-the-middle:
specifier: 1.14.2
version: 1.14.2
msw: msw:
specifier: 2.11.6 specifier: 2.11.6
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3) version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
@@ -986,12 +983,15 @@ packages:
'@date-fns/tz@1.4.1': '@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@emnapi/core@1.5.0': '@emnapi/core@1.7.1':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
'@emnapi/runtime@1.5.0': '@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
'@emnapi/wasi-threads@1.1.0': '@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -1329,6 +1329,10 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/eslintrc@2.1.4': '@eslint/eslintrc@2.1.4':
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1605,8 +1609,8 @@ packages:
'@next/env@15.4.10': '@next/env@15.4.10':
resolution: {integrity: sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==} resolution: {integrity: sha512-knhmoJ0Vv7VRf6pZEPSnciUG1S4bIhWx+qTYBW/AjxEtlzsiNORPk8sFDCEvqLfmKuey56UB9FL1UdHEV3uBrg==}
'@next/eslint-plugin-next@15.5.2': '@next/eslint-plugin-next@15.5.7':
resolution: {integrity: sha512-lkLrRVxcftuOsJNhWatf1P2hNVfh98k/omQHrCEPPriUypR6RcS13IvLdIrEvkm9AH2Nu2YpR5vLqBuy6twH3Q==} resolution: {integrity: sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==}
'@next/swc-darwin-arm64@15.4.8': '@next/swc-darwin-arm64@15.4.8':
resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==} resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==}
@@ -2622,8 +2626,8 @@ packages:
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/eslint-patch@1.12.0': '@rushstack/eslint-patch@1.15.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==}
'@scarf/scarf@1.4.0': '@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
@@ -3097,8 +3101,8 @@ packages:
peerDependencies: peerDependencies:
'@testing-library/dom': '>=7.21.4' '@testing-library/dom': '>=7.21.4'
'@tybys/wasm-util@0.10.0': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/aria-query@5.0.4': '@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@@ -3288,16 +3292,16 @@ packages:
'@types/ws@8.18.1': '@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript-eslint/eslint-plugin@8.43.0': '@typescript-eslint/eslint-plugin@8.48.1':
resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^8.43.0 '@typescript-eslint/parser': ^8.48.1
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.43.0': '@typescript-eslint/parser@8.48.1':
resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
@@ -3315,6 +3319,12 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.48.1':
resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.43.0': '@typescript-eslint/scope-manager@8.43.0':
resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3323,6 +3333,10 @@ packages:
resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.48.1':
resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.43.0': '@typescript-eslint/tsconfig-utils@8.43.0':
resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3335,8 +3349,14 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.43.0': '@typescript-eslint/tsconfig-utils@8.48.1':
resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.48.1':
resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
@@ -3350,6 +3370,10 @@ packages:
resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.48.1':
resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.43.0': '@typescript-eslint/typescript-estree@8.43.0':
resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3362,6 +3386,12 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.48.1':
resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.43.0': '@typescript-eslint/utils@8.43.0':
resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3376,6 +3406,13 @@ packages:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.48.1':
resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.43.0': '@typescript-eslint/visitor-keys@8.43.0':
resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3384,6 +3421,10 @@ packages:
resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.48.1':
resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -4585,8 +4626,8 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'} engines: {node: '>=12'}
eslint-config-next@15.5.2: eslint-config-next@15.5.7:
resolution: {integrity: sha512-3hPZghsLupMxxZ2ggjIIrat/bPniM2yRpsVPVM40rp8ZMzKWOJp2CGWn7+EzoV2ddkUr5fxNfHpF+wU1hGt/3g==} resolution: {integrity: sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==}
peerDependencies: peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1' typescript: '>=3.3.1'
@@ -4918,6 +4959,10 @@ packages:
peerDependencies: peerDependencies:
next: '>=13.2.0' next: '>=13.2.0'
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -4946,8 +4991,8 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
get-tsconfig@4.10.1: get-tsconfig@4.13.0:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
github-slugger@2.0.0: github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
@@ -5168,9 +5213,6 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
import-in-the-middle@1.14.2:
resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==}
import-in-the-middle@2.0.0: import-in-the-middle@2.0.0:
resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==}
@@ -5282,6 +5324,10 @@ packages:
resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-generator-function@1.1.2:
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
engines: {node: '>= 0.4'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -5903,8 +5949,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
napi-postinstall@0.3.3: napi-postinstall@0.3.4:
resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
hasBin: true hasBin: true
@@ -6769,6 +6815,11 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hasBin: true hasBin: true
resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'}
hasBin: true
resolve@1.22.8: resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true hasBin: true
@@ -7858,7 +7909,7 @@ snapshots:
'@babel/helper-plugin-utils': 7.27.1 '@babel/helper-plugin-utils': 7.27.1
debug: 4.4.3 debug: 4.4.3
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
resolve: 1.22.10 resolve: 1.22.11
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -8550,7 +8601,7 @@ snapshots:
'@date-fns/tz@1.4.1': {} '@date-fns/tz@1.4.1': {}
'@emnapi/core@1.5.0': '@emnapi/core@1.7.1':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.1.0 '@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1 tslib: 2.8.1
@@ -8561,6 +8612,11 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@emnapi/runtime@1.7.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0': '@emnapi/wasi-threads@1.1.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -8739,6 +8795,8 @@ snapshots:
'@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.1': {}
'@eslint-community/regexpp@4.12.2': {}
'@eslint/eslintrc@2.1.4': '@eslint/eslintrc@2.1.4':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
@@ -8996,16 +9054,16 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
dependencies: dependencies:
'@emnapi/core': 1.5.0 '@emnapi/core': 1.7.1
'@emnapi/runtime': 1.5.0 '@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.0 '@tybys/wasm-util': 0.10.1
optional: true optional: true
'@neoconfetti/react@1.0.0': {} '@neoconfetti/react@1.0.0': {}
'@next/env@15.4.10': {} '@next/env@15.4.10': {}
'@next/eslint-plugin-next@15.5.2': '@next/eslint-plugin-next@15.5.7':
dependencies: dependencies:
fast-glob: 3.3.1 fast-glob: 3.3.1
@@ -10115,7 +10173,7 @@ snapshots:
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.12.0': {} '@rushstack/eslint-patch@1.15.0': {}
'@scarf/scarf@1.4.0': {} '@scarf/scarf@1.4.0': {}
@@ -10867,7 +10925,7 @@ snapshots:
dependencies: dependencies:
'@testing-library/dom': 10.4.1 '@testing-library/dom': 10.4.1
'@tybys/wasm-util@0.10.0': '@tybys/wasm-util@0.10.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
@@ -11065,14 +11123,14 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.10.0 '@types/node': 24.10.0
'@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.43.0 '@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/type-utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/type-utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.43.0 '@typescript-eslint/visitor-keys': 8.48.1
eslint: 8.57.1 eslint: 8.57.1
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 7.0.5 ignore: 7.0.5
@@ -11082,12 +11140,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3)': '@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.43.0 '@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/types': 8.43.0 '@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.43.0 '@typescript-eslint/visitor-keys': 8.48.1
debug: 4.4.3 debug: 4.4.3
eslint: 8.57.1 eslint: 8.57.1
typescript: 5.9.3 typescript: 5.9.3
@@ -11097,7 +11155,7 @@ snapshots:
'@typescript-eslint/project-service@8.43.0(typescript@5.9.3)': '@typescript-eslint/project-service@8.43.0(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.3)
'@typescript-eslint/types': 8.43.0 '@typescript-eslint/types': 8.48.1
debug: 4.4.3 debug: 4.4.3
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -11106,7 +11164,16 @@ snapshots:
'@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
'@typescript-eslint/types': 8.46.2 '@typescript-eslint/types': 8.48.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.48.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
'@typescript-eslint/types': 8.48.1
debug: 4.4.3 debug: 4.4.3
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -11122,6 +11189,11 @@ snapshots:
'@typescript-eslint/types': 8.46.2 '@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2 '@typescript-eslint/visitor-keys': 8.46.2
'@typescript-eslint/scope-manager@8.48.1':
dependencies:
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/visitor-keys': 8.48.1
'@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.3)': '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.3)':
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
@@ -11130,11 +11202,15 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/type-utils@8.43.0(eslint@8.57.1)(typescript@5.9.3)': '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.43.0 typescript: 5.9.3
'@typescript-eslint/typescript-estree': 8.43.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/type-utils@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3 debug: 4.4.3
eslint: 8.57.1 eslint: 8.57.1
ts-api-utils: 2.1.0(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -11146,6 +11222,8 @@ snapshots:
'@typescript-eslint/types@8.46.2': {} '@typescript-eslint/types@8.46.2': {}
'@typescript-eslint/types@8.48.1': {}
'@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.3)': '@typescript-eslint/typescript-estree@8.43.0(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.43.0(typescript@5.9.3) '@typescript-eslint/project-service': 8.43.0(typescript@5.9.3)
@@ -11156,7 +11234,7 @@ snapshots:
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
semver: 7.7.2 semver: 7.7.3
ts-api-utils: 2.1.0(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -11178,6 +11256,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.48.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/visitor-keys': 8.48.1
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.3)': '@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
@@ -11200,6 +11293,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.48.1
'@typescript-eslint/types': 8.48.1
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.43.0': '@typescript-eslint/visitor-keys@8.43.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.43.0 '@typescript-eslint/types': 8.43.0
@@ -11210,6 +11314,11 @@ snapshots:
'@typescript-eslint/types': 8.46.2 '@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.48.1':
dependencies:
'@typescript-eslint/types': 8.48.1
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@unrs/resolver-binding-android-arm-eabi@1.11.1': '@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -12532,16 +12641,16 @@ snapshots:
escape-string-regexp@5.0.0: {} escape-string-regexp@5.0.0: {}
eslint-config-next@15.5.2(eslint@8.57.1)(typescript@5.9.3): eslint-config-next@15.5.7(eslint@8.57.1)(typescript@5.9.3):
dependencies: dependencies:
'@next/eslint-plugin-next': 15.5.2 '@next/eslint-plugin-next': 15.5.7
'@rushstack/eslint-patch': 1.12.0 '@rushstack/eslint-patch': 1.15.0
'@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -12556,7 +12665,7 @@ snapshots:
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
is-core-module: 2.16.1 is-core-module: 2.16.1
resolve: 1.22.10 resolve: 1.22.11
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -12565,28 +12674,28 @@ snapshots:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
eslint: 8.57.1 eslint: 8.57.1
get-tsconfig: 4.10.1 get-tsconfig: 4.13.0
is-bun-module: 2.0.0 is-bun-module: 2.0.0
stable-hash: 0.0.5 stable-hash: 0.0.5
tinyglobby: 0.2.15 tinyglobby: 0.2.15
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -12597,7 +12706,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -12609,7 +12718,7 @@ snapshots:
string.prototype.trimend: 1.0.9 string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0 tsconfig-paths: 3.15.0
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@8.57.1)(typescript@5.9.3)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
@@ -12958,6 +13067,8 @@ snapshots:
dependencies: dependencies:
next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: 15.4.10(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
@@ -12990,7 +13101,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
get-tsconfig@4.10.1: get-tsconfig@4.13.0:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
@@ -13274,13 +13385,6 @@ snapshots:
parent-module: 1.0.1 parent-module: 1.0.1
resolve-from: 4.0.0 resolve-from: 4.0.0
import-in-the-middle@1.14.2:
dependencies:
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
cjs-module-lexer: 1.4.3
module-details-from-path: 1.0.4
import-in-the-middle@2.0.0: import-in-the-middle@2.0.0:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0
@@ -13357,7 +13461,7 @@ snapshots:
is-bun-module@2.0.0: is-bun-module@2.0.0:
dependencies: dependencies:
semver: 7.7.2 semver: 7.7.3
is-callable@1.2.7: {} is-callable@1.2.7: {}
@@ -13395,6 +13499,14 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
safe-regex-test: 1.1.0 safe-regex-test: 1.1.0
is-generator-function@1.1.2:
dependencies:
call-bound: 1.0.4
generator-function: 2.0.1
get-proto: 1.0.1
has-tostringtag: 1.0.2
safe-regex-test: 1.1.0
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@@ -14215,7 +14327,7 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
napi-postinstall@0.3.3: {} napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -15185,6 +15297,12 @@ snapshots:
path-parse: 1.0.7 path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0 supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.11:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.8: resolve@1.22.8:
dependencies: dependencies:
is-core-module: 2.16.1 is-core-module: 2.16.1
@@ -15996,7 +16114,7 @@ snapshots:
unrs-resolver@1.11.1: unrs-resolver@1.11.1:
dependencies: dependencies:
napi-postinstall: 0.3.3 napi-postinstall: 0.3.4
optionalDependencies: optionalDependencies:
'@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm-eabi': 1.11.1
'@unrs/resolver-binding-android-arm64': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1
@@ -16224,7 +16342,7 @@ snapshots:
is-async-function: 2.1.1 is-async-function: 2.1.1
is-date-object: 1.1.0 is-date-object: 1.1.0
is-finalizationregistry: 1.1.1 is-finalizationregistry: 1.1.1
is-generator-function: 1.1.0 is-generator-function: 1.1.2
is-regex: 1.2.1 is-regex: 1.2.1
is-weakref: 1.1.1 is-weakref: 1.1.1
isarray: 2.0.5 isarray: 2.0.5

View File

@@ -8,7 +8,6 @@ import {
CardTitle, CardTitle,
} from "@/components/__legacy__/ui/card"; } from "@/components/__legacy__/ui/card";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr"; import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { Play } from "lucide-react"; import { Play } from "lucide-react";
import OnboardingButton from "../components/OnboardingButton"; import OnboardingButton from "../components/OnboardingButton";
@@ -79,20 +78,13 @@ export default function Page() {
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{Object.entries(agent?.input_schema.properties || {}).map( {Object.entries(agent?.input_schema.properties || {}).map(
([key, inputSubSchema]) => ( ([key, inputSubSchema]) => (
<div key={key} className="flex flex-col space-y-2"> <RunAgentInputs
<label className="flex items-center gap-1 text-sm font-medium"> key={key}
{inputSubSchema.title || key} schema={inputSubSchema}
<InformationTooltip value={onboarding.state?.agentInput?.[key]}
description={inputSubSchema.description} placeholder={inputSubSchema.description}
/> onChange={(value) => handleSetAgentInput(key, value)}
</label> />
<RunAgentInputs
schema={inputSubSchema}
value={onboarding.state?.agentInput?.[key]}
placeholder={inputSubSchema.description}
onChange={(value) => handleSetAgentInput(key, value)}
/>
</div>
), ),
)} )}
<AgentOnboardingCredentials <AgentOnboardingCredentials

View File

@@ -2,6 +2,7 @@ import { parseAsString, useQueryStates } from "nuqs";
import { AgentOutputs } from "./components/AgentOutputs/AgentOutputs"; import { AgentOutputs } from "./components/AgentOutputs/AgentOutputs";
import { RunGraph } from "./components/RunGraph/RunGraph"; import { RunGraph } from "./components/RunGraph/RunGraph";
import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph"; import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph";
import { PublishToMarketplace } from "./components/PublishToMarketplace/PublishToMarketplace";
import { memo } from "react"; import { memo } from "react";
export const BuilderActions = memo(() => { export const BuilderActions = memo(() => {
@@ -13,6 +14,7 @@ export const BuilderActions = memo(() => {
<AgentOutputs flowID={flowID} /> <AgentOutputs flowID={flowID} />
<RunGraph flowID={flowID} /> <RunGraph flowID={flowID} />
<ScheduleGraph flowID={flowID} /> <ScheduleGraph flowID={flowID} />
<PublishToMarketplace flowID={flowID} />
</div> </div>
); );
}); });

View File

@@ -0,0 +1,36 @@
import { ShareIcon } from "@phosphor-icons/react";
import { BuilderActionButton } from "../BuilderActionButton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { usePublishToMarketplace } from "./usePublishToMarketplace";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
export const PublishToMarketplace = ({ flowID }: { flowID: string | null }) => {
const { handlePublishToMarketplace, publishState, handleStateChange } =
usePublishToMarketplace({ flowID });
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<BuilderActionButton
onClick={handlePublishToMarketplace}
disabled={!flowID}
>
<ShareIcon className="size-6 drop-shadow-sm" />
</BuilderActionButton>
</TooltipTrigger>
<TooltipContent>Publish to Marketplace</TooltipContent>
</Tooltip>
<PublishAgentModal
targetState={publishState}
onStateChange={handleStateChange}
preSelectedAgentId={flowID || undefined}
/>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { useState, useCallback } from "react";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
export type PublishStep = "select" | "info" | "review";
export type PublishState = {
isOpen: boolean;
step: PublishStep;
submissionData: StoreSubmission | null;
};
const defaultPublishState: PublishState = {
isOpen: false,
step: "select",
submissionData: null,
};
interface UsePublishToMarketplaceProps {
flowID: string | null;
}
export function usePublishToMarketplace({
flowID,
}: UsePublishToMarketplaceProps) {
const [publishState, setPublishState] =
useState<PublishState>(defaultPublishState);
const handlePublishToMarketplace = () => {
if (!flowID) return;
// Open the publish modal starting with the select step
setPublishState({
isOpen: true,
step: "select",
submissionData: null,
});
};
const handleStateChange = useCallback((newState: PublishState) => {
setPublishState(newState);
}, []);
return {
handlePublishToMarketplace,
publishState,
handleStateChange,
};
}

View File

@@ -20,6 +20,7 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
export const useFlow = () => { export const useFlow = () => {
const [isLocked, setIsLocked] = useState(false); const [isLocked, setIsLocked] = useState(false);
const [hasAutoFramed, setHasAutoFramed] = useState(false);
const addNodes = useNodeStore(useShallow((state) => state.addNodes)); const addNodes = useNodeStore(useShallow((state) => state.addNodes));
const addLinks = useEdgeStore(useShallow((state) => state.addLinks)); const addLinks = useEdgeStore(useShallow((state) => state.addLinks));
const updateNodeStatus = useNodeStore( const updateNodeStatus = useNodeStore(
@@ -187,9 +188,36 @@ export const useFlow = () => {
}; };
}, []); }, []);
const linkCount = graph?.links?.length ?? 0;
useEffect(() => { useEffect(() => {
fitView({ padding: 0.2, duration: 800, maxZoom: 2 }); if (isGraphLoading || isBlocksLoading) {
}, [fitView]); setHasAutoFramed(false);
return;
}
if (hasAutoFramed) {
return;
}
const rafId = requestAnimationFrame(() => {
fitView({ padding: 0.2, duration: 800, maxZoom: 1 });
setHasAutoFramed(true);
});
return () => cancelAnimationFrame(rafId);
}, [
fitView,
hasAutoFramed,
customNodes.length,
isBlocksLoading,
isGraphLoading,
linkCount,
]);
useEffect(() => {
setHasAutoFramed(false);
}, [flowID, flowVersion]);
// Drag and drop block from block menu // Drag and drop block from block menu
const onDragOver = useCallback((event: React.DragEvent) => { const onDragOver = useCallback((event: React.DragEvent) => {

View File

@@ -106,7 +106,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
/> />
<NodeAdvancedToggle nodeId={nodeId} /> <NodeAdvancedToggle nodeId={nodeId} />
{data.uiType != BlockUIType.OUTPUT && ( {data.uiType != BlockUIType.OUTPUT && (
<OutputHandler outputSchema={outputSchema} nodeId={nodeId} /> <OutputHandler
uiType={data.uiType}
outputSchema={outputSchema}
nodeId={nodeId}
/>
)} )}
<NodeDataRenderer nodeId={nodeId} /> <NodeDataRenderer nodeId={nodeId} />
</div> </div>

View File

@@ -20,17 +20,32 @@ export const FormCreator = React.memo(
className?: string; className?: string;
}) => { }) => {
const updateNodeData = useNodeStore((state) => state.updateNodeData); const updateNodeData = useNodeStore((state) => state.updateNodeData);
const getHardCodedValues = useNodeStore( const getHardCodedValues = useNodeStore(
(state) => state.getHardCodedValues, (state) => state.getHardCodedValues,
); );
const handleChange = ({ formData }: any) => { const handleChange = ({ formData }: any) => {
if ("credentials" in formData && !formData.credentials?.id) { if ("credentials" in formData && !formData.credentials?.id) {
delete formData.credentials; delete formData.credentials;
} }
updateNodeData(nodeId, { hardcodedValues: formData });
const updatedValues =
uiType === BlockUIType.AGENT
? {
...getHardCodedValues(nodeId),
inputs: formData,
}
: formData;
updateNodeData(nodeId, { hardcodedValues: updatedValues });
}; };
const initialValues = getHardCodedValues(nodeId); const hardcodedValues = getHardCodedValues(nodeId);
const initialValues =
uiType === BlockUIType.AGENT
? (hardcodedValues.inputs ?? {})
: hardcodedValues;
return ( return (
<div className={className}> <div className={className}>

View File

@@ -14,13 +14,16 @@ import {
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore"; import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { getTypeDisplayInfo } from "./helpers"; import { getTypeDisplayInfo } from "./helpers";
import { generateHandleId } from "../handlers/helpers"; import { generateHandleId } from "../handlers/helpers";
import { BlockUIType } from "../../types";
export const OutputHandler = ({ export const OutputHandler = ({
outputSchema, outputSchema,
nodeId, nodeId,
uiType,
}: { }: {
outputSchema: RJSFSchema; outputSchema: RJSFSchema;
nodeId: string; nodeId: string;
uiType: BlockUIType;
}) => { }) => {
const { isOutputConnected } = useEdgeStore(); const { isOutputConnected } = useEdgeStore();
const properties = outputSchema?.properties || {}; const properties = outputSchema?.properties || {};
@@ -79,7 +82,9 @@ export const OutputHandler = ({
</Text> </Text>
<NodeHandle <NodeHandle
handleId={generateHandleId(key)} handleId={
uiType === BlockUIType.AGENT ? key : generateHandleId(key)
}
isConnected={isConnected} isConnected={isConnected}
side="right" side="right"
/> />

View File

@@ -7,6 +7,7 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { getV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store"; import { getV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import { import {
getGetV2ListLibraryAgentsQueryKey, getGetV2ListLibraryAgentsQueryKey,
getV2GetLibraryAgent,
usePostV2AddMarketplaceAgent, usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library"; } from "@/app/api/__generated__/endpoints/library/library";
import { import {
@@ -151,7 +152,12 @@ export const useBlockMenuSearch = () => {
}); });
const libraryAgent = response.data as LibraryAgent; const libraryAgent = response.data as LibraryAgent;
addAgentToBuilder(libraryAgent);
const { data: libraryAgentDetails } = await getV2GetLibraryAgent(
libraryAgent.id,
);
addAgentToBuilder(libraryAgentDetails as LibraryAgent);
toast({ toast({
title: "Agent Added", title: "Agent Added",

View File

@@ -1,6 +1,7 @@
import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default"; import { getGetV2GetBuilderItemCountsQueryKey } from "@/app/api/__generated__/endpoints/default/default";
import { import {
getGetV2ListLibraryAgentsQueryKey, getGetV2ListLibraryAgentsQueryKey,
getV2GetLibraryAgent,
usePostV2AddMarketplaceAgent, usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library"; } from "@/app/api/__generated__/endpoints/library/library";
import { import {
@@ -105,8 +106,16 @@ export const useMarketplaceAgentsContent = () => {
}, },
}); });
// Here, libraryAgent has empty input and output schemas.
// Not updating the endpoint because this endpoint is used elsewhere.
// TODO: Create a new endpoint for builder specific to marketplace agents.
const libraryAgent = response.data as LibraryAgent; const libraryAgent = response.data as LibraryAgent;
addAgentToBuilder(libraryAgent);
const { data: libraryAgentDetails } = await getV2GetLibraryAgent(
libraryAgent.id,
);
addAgentToBuilder(libraryAgentDetails as LibraryAgent);
toast({ toast({
title: "Agent Added", title: "Agent Added",

View File

@@ -103,6 +103,7 @@ const FlowEditor: React.FC<{
updateNode, updateNode,
getViewport, getViewport,
setViewport, setViewport,
fitView,
screenToFlowPosition, screenToFlowPosition,
} = useReactFlow<CustomNode, CustomEdge>(); } = useReactFlow<CustomNode, CustomEdge>();
const [nodeId, setNodeId] = useState<number>(1); const [nodeId, setNodeId] = useState<number>(1);
@@ -115,6 +116,7 @@ const FlowEditor: React.FC<{
const [pinBlocksPopover, setPinBlocksPopover] = useState(false); const [pinBlocksPopover, setPinBlocksPopover] = useState(false);
// State to control if save popover should be pinned open // State to control if save popover should be pinned open
const [pinSavePopover, setPinSavePopover] = useState(false); const [pinSavePopover, setPinSavePopover] = useState(false);
const [hasAutoFramed, setHasAutoFramed] = useState(false);
const { const {
agentName, agentName,
@@ -482,35 +484,26 @@ const FlowEditor: React.FC<{
return uuidv4(); return uuidv4();
}, []); }, []);
// Set the initial view port to center the canvas.
useEffect(() => { useEffect(() => {
const { x, y } = getViewport(); if (nodes.length === 0) {
if (nodes.length <= 0 || x !== 0 || y !== 0) {
return; return;
} }
const topLeft = { x: Infinity, y: Infinity }; if (hasAutoFramed) {
const bottomRight = { x: -Infinity, y: -Infinity }; return;
}
nodes.forEach((node) => { const rafId = requestAnimationFrame(() => {
const { x, y } = node.position; fitView({ padding: 0.2, duration: 800, maxZoom: 1 });
topLeft.x = Math.min(topLeft.x, x); setHasAutoFramed(true);
topLeft.y = Math.min(topLeft.y, y);
// Rough estimate of the width and height of the node: 500x400.
bottomRight.x = Math.max(bottomRight.x, x + 500);
bottomRight.y = Math.max(bottomRight.y, y + 400);
}); });
const centerX = (topLeft.x + bottomRight.x) / 2; return () => cancelAnimationFrame(rafId);
const centerY = (topLeft.y + bottomRight.y) / 2; }, [fitView, hasAutoFramed, nodes.length]);
const zoom = 0.8;
setViewport({ useEffect(() => {
x: window.innerWidth / 2 - centerX * zoom, setHasAutoFramed(false);
y: window.innerHeight / 2 - centerY * zoom, }, [flowID, flowVersion]);
zoom: zoom,
});
}, [nodes, getViewport, setViewport]);
const navigateToNode = useCallback( const navigateToNode = useCallback(
(nodeId: string) => { (nodeId: string) => {

View File

@@ -0,0 +1,102 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
interface MarketplaceBannersProps {
hasUpdate?: boolean;
latestVersion?: number;
hasUnpublishedChanges?: boolean;
currentVersion?: number;
isUpdating?: boolean;
onUpdate?: () => void;
onPublish?: () => void;
onViewChanges?: () => void;
}
export function MarketplaceBanners({
hasUpdate,
latestVersion,
hasUnpublishedChanges,
isUpdating,
onUpdate,
onPublish,
}: MarketplaceBannersProps) {
const renderUpdateBanner = () => {
if (hasUpdate && latestVersion) {
return (
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
<div className="flex flex-col gap-3">
<div>
<Text
variant="large-medium"
className="mb-2 text-neutral-900 dark:text-neutral-100"
>
Update available
</Text>
<Text variant="body" className="text-gray-700 dark:text-gray-300">
You should update your agent in order to get the latest / best
results
</Text>
</div>
{onUpdate && (
<div className="flex justify-start">
<Button
size="small"
onClick={onUpdate}
disabled={isUpdating}
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
>
{isUpdating ? "Updating..." : "Update agent"}
</Button>
</div>
)}
</div>
</div>
);
}
return null;
};
const renderUnpublishedChangesBanner = () => {
if (hasUnpublishedChanges) {
return (
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
<div className="flex flex-col gap-3">
<div>
<Text
variant="large-medium"
className="mb-2 text-neutral-900 dark:text-neutral-100"
>
Unpublished changes
</Text>
<Text variant="body" className="text-gray-700 dark:text-gray-300">
You&apos;ve made changes to this agent that aren&apos;t
published yet. Would you like to publish the latest version?
</Text>
</div>
{onPublish && (
<div className="flex justify-start">
<Button
size="small"
onClick={onPublish}
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
>
Publish changes
</Button>
</div>
)}
</div>
</div>
);
}
return null;
};
return (
<>
{renderUpdateBanner()}
{renderUnpublishedChangesBanner()}
</>
);
}

View File

@@ -1,14 +1,18 @@
"use client"; "use client";
import { Button } from "@/components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PlusIcon } from "@phosphor-icons/react"; import { PlusIcon } from "@phosphor-icons/react";
import * as React from "react";
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal"; import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
import { MarketplaceBanners } from "../../../../../components/MarketplaceBanners/MarketplaceBanners";
import { AgentRunsLoading } from "./components/other/AgentRunsLoading"; import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
import { EmptySchedules } from "./components/other/EmptySchedules"; import { EmptySchedules } from "./components/other/EmptySchedules";
import { EmptyTasks } from "./components/other/EmptyTasks"; import { EmptyTasks } from "./components/other/EmptyTasks";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { EmptyTemplates } from "./components/other/EmptyTemplates"; import { EmptyTemplates } from "./components/other/EmptyTemplates";
import { EmptyTriggers } from "./components/other/EmptyTriggers"; import { EmptyTriggers } from "./components/other/EmptyTriggers";
import { SectionWrap } from "./components/other/SectionWrap"; import { SectionWrap } from "./components/other/SectionWrap";
@@ -24,7 +28,6 @@ import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
export function NewAgentLibraryView() { export function NewAgentLibraryView() {
const { const {
agentId,
agent, agent,
ready, ready,
activeTemplate, activeTemplate,
@@ -43,6 +46,189 @@ export function NewAgentLibraryView() {
onScheduleCreated, onScheduleCreated,
} = useNewAgentLibraryView(); } = useNewAgentLibraryView();
const {
hasAgentMarketplaceUpdate,
hasMarketplaceUpdate,
latestMarketplaceVersion,
isUpdating,
modalOpen,
setModalOpen,
handlePublishUpdate,
handleUpdateToLatest,
} = useMarketplaceUpdate({ agent });
const [changelogOpen, setChangelogOpen] = React.useState(false);
function renderMarketplaceUpdateBanner() {
return (
<MarketplaceBanners
hasUpdate={!!hasMarketplaceUpdate}
latestVersion={latestMarketplaceVersion}
hasUnpublishedChanges={!!hasAgentMarketplaceUpdate}
currentVersion={agent?.graph_version}
isUpdating={isUpdating}
onUpdate={handleUpdateToLatest}
onPublish={handlePublishUpdate}
onViewChanges={() => setChangelogOpen(true)}
/>
);
}
function renderPublishAgentModal() {
if (!modalOpen || !agent) return null;
return (
<PublishAgentModal
targetState={{
isOpen: true,
step: "info",
submissionData: { isMarketplaceUpdate: true } as any,
}}
preSelectedAgentId={agent.graph_id}
preSelectedAgentVersion={agent.graph_version}
onStateChange={(state) => {
if (!state.isOpen) {
setModalOpen(false);
}
}}
/>
);
}
function renderAgentLibraryView() {
if (!agent) return null;
return (
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
<SectionWrap className="mb-3 block">
<div
className={cn(
"border-b border-zinc-100 pb-5",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="w-full"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
</div>
<SidebarRunsList
agent={agent}
selectedRunId={activeItem ?? undefined}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
onTabChange={setActiveTab}
onCountsChange={handleCountsChange}
/>
</SectionWrap>
{activeItem ? (
activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
banner={renderMarketplaceUpdateBanner()}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
templateId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onRunCreated={(execution) =>
handleSelectRun(execution.id, "runs")
}
onSwitchToRunsTab={() => setActiveTab("runs")}
banner={renderMarketplaceUpdateBanner()}
/>
) : activeTab === "triggers" ? (
<SelectedTriggerView
agent={agent}
triggerId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onSwitchToRunsTab={() => setActiveTab("runs")}
banner={renderMarketplaceUpdateBanner()}
/>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
banner={renderMarketplaceUpdateBanner()}
/>
)
) : sidebarLoading ? (
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
) : activeTab === "scheduled" ? (
<SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={renderMarketplaceUpdateBanner()}
>
<EmptySchedules />
</SelectedViewLayout>
) : activeTab === "templates" ? (
<SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTemplates />
</SelectedViewLayout>
) : activeTab === "triggers" ? (
<SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTriggers />
</SelectedViewLayout>
) : (
<SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</SelectedViewLayout>
)}
</div>
);
}
function renderVersionChangelog() {
if (!agent) return null;
return (
<AgentVersionChangelog
agent={agent}
isOpen={changelogOpen}
onClose={() => setChangelogOpen(false)}
/>
);
}
if (error) { if (error) {
return ( return (
<ErrorCard <ErrorCard
@@ -60,120 +246,30 @@ export function NewAgentLibraryView() {
if (!sidebarLoading && !hasAnyItems) { if (!sidebarLoading && !hasAnyItems) {
return ( return (
<div className="flex h-full flex-col"> <>
<div className="mx-6 pt-4"> <SelectedViewLayout
<Breadcrumbs agentName={agent.name}
items={[ agentId={agent.id}
{ name: "My Library", link: "/library" }, banner={renderMarketplaceUpdateBanner()}
{ name: agent.name, link: `/library/agents/${agentId}` }, >
]}
/>
</div>
<div className="flex min-h-0 flex-1">
<EmptyTasks <EmptyTasks
agent={agent} agent={agent}
onRun={onRunInitiated} onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup} onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated} onScheduleCreated={onScheduleCreated}
/> />
</div> </SelectedViewLayout>
</div> {renderPublishAgentModal()}
{renderVersionChangelog()}
</>
); );
} }
return ( return (
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]"> <>
<SectionWrap className="mb-3 block"> {renderAgentLibraryView()}
<div {renderPublishAgentModal()}
className={cn( {renderVersionChangelog()}
"border-b border-zinc-100 pb-5", </>
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="w-full"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
</div>
<SidebarRunsList
agent={agent}
selectedRunId={activeItem ?? undefined}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
onTabChange={setActiveTab}
onCountsChange={handleCountsChange}
/>
</SectionWrap>
{activeItem ? (
activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
templateId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : activeTab === "triggers" ? (
<SelectedTriggerView
agent={agent}
triggerId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
/>
)
) : sidebarLoading ? (
<LoadingSelectedContent agentName={agent.name} agentId={agent.id} />
) : activeTab === "scheduled" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptySchedules />
</SelectedViewLayout>
) : activeTab === "templates" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTemplates />
</SelectedViewLayout>
) : activeTab === "triggers" ? (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTriggers />
</SelectedViewLayout>
) : (
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</SelectedViewLayout>
)}
</div>
); );
} }

View File

@@ -0,0 +1,137 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import React from "react";
interface AgentVersionChangelogProps {
agent: LibraryAgent;
isOpen: boolean;
onClose: () => void;
}
interface VersionInfo {
version: number;
isCurrentVersion: boolean;
}
export function AgentVersionChangelog({
agent,
isOpen,
onClose,
}: AgentVersionChangelogProps) {
// Get marketplace data if agent has marketplace listing
const { data: storeAgentData, isLoading } = useGetV2GetSpecificAgent(
agent?.marketplace_listing?.creator.slug || "",
agent?.marketplace_listing?.slug || "",
{},
{
query: {
enabled: !!(
agent?.marketplace_listing?.creator.slug &&
agent?.marketplace_listing?.slug
),
},
},
);
// Create version info from available graph versions
const storeData = okData<StoreAgentDetails>(storeAgentData);
const agentVersions: VersionInfo[] = storeData?.agentGraphVersions
? storeData.agentGraphVersions
.map((versionStr: string) => parseInt(versionStr, 10))
.sort((a: number, b: number) => b - a) // Sort descending (newest first)
.map((version: number) => ({
version,
isCurrentVersion: version === agent.graph_version,
}))
: [];
const renderVersionItem = (versionInfo: VersionInfo) => {
return (
<div
key={versionInfo.version}
className={`rounded-lg border p-4 ${
versionInfo.isCurrentVersion
? "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950"
: "border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Text variant="body" className="font-semibold">
v{versionInfo.version}
</Text>
{versionInfo.isCurrentVersion && (
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
Current
</span>
)}
</div>
</div>
<Text
variant="small"
className="mt-1 text-neutral-600 dark:text-neutral-400"
>
Available marketplace version
</Text>
</div>
);
};
return (
<Dialog
title={`Version History - ${agent.name}`}
styling={{
maxWidth: "45rem",
}}
controlled={{
isOpen: isOpen,
set: (isOpen) => {
if (!isOpen) {
onClose();
}
},
}}
>
<Dialog.Content>
<div className="max-h-[70vh] overflow-y-auto">
{isLoading ? (
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
) : agentVersions.length > 0 ? (
<div className="space-y-4">
<Text
variant="small"
className="text-neutral-600 dark:text-neutral-400"
>
View changes and updates across different versions of this
agent.
</Text>
{agentVersions.map(renderVersionItem)}
</div>
) : (
<div className="py-8 text-center">
<Text
variant="body"
className="text-neutral-600 dark:text-neutral-400"
>
No version history available for this agent.
</Text>
</div>
)}
</div>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,16 +1,11 @@
"use client"; "use client";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { import { Text } from "@/components/atoms/Text/Text";
BlockIOSubSchema, import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs"; import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
import { import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
getAgentCredentialsFields, import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
getAgentInputFields,
renderValue,
} from "./helpers";
type Props = { type Props = {
agent: LibraryAgent; agent: LibraryAgent;
@@ -28,19 +23,23 @@ export function AgentInputsReadOnly({
getAgentCredentialsFields(agent), getAgentCredentialsFields(agent),
); );
// Take actual input entries as leading; augment with schema from input fields.
// TODO: ensure consistent ordering.
const inputEntries = const inputEntries =
inputs && inputs &&
Object.entries(inputs).map<[string, [BlockIOSubSchema | undefined, any]]>( Object.entries(inputs).map(([key, value]) => ({
([k, v]) => [k, [inputFields[k], v]], key,
); schema: inputFields[key],
value,
}));
const hasInputs = inputEntries && inputEntries.length > 0; const hasInputs = inputEntries && inputEntries.length > 0;
const hasCredentials = credentialInputs && credentialFieldEntries.length > 0; const hasCredentials = credentialInputs && credentialFieldEntries.length > 0;
if (!hasInputs && !hasCredentials) { if (!hasInputs && !hasCredentials) {
return <div className="text-neutral-600">No input for this run.</div>; return (
<Text variant="body" className="text-zinc-700">
No input for this run.
</Text>
);
} }
return ( return (
@@ -48,16 +47,20 @@ export function AgentInputsReadOnly({
{/* Regular inputs */} {/* Regular inputs */}
{hasInputs && ( {hasInputs && (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputEntries.map(([key, [schema, value]]) => ( {inputEntries.map(({ key, schema, value }) => {
<div key={key} className="flex flex-col gap-1.5"> if (!schema) return null;
<label className="text-sm font-medium">
{schema?.title || key} return (
</label> <RunAgentInputs
<p className="whitespace-pre-wrap break-words text-sm text-neutral-700"> key={key}
{renderValue(value)} schema={schema}
</p> value={value}
</div> placeholder={schema.description}
))} onChange={() => {}}
readOnly={true}
/>
);
})}
</div> </div>
)} )}

View File

@@ -62,12 +62,15 @@ export function CredentialRow({
</div> </div>
<IconKey className="h-5 w-5 shrink-0 text-zinc-800" /> <IconKey className="h-5 w-5 shrink-0 text-zinc-800" />
<div className="flex min-w-0 flex-1 flex-nowrap items-center gap-4"> <div className="flex min-w-0 flex-1 flex-nowrap items-center gap-4">
<Text variant="body" className="tracking-tight"> <Text
variant="body"
className="line-clamp-1 flex-[0_0_50%] text-ellipsis tracking-tight"
>
{getCredentialDisplayName(credential, displayName)} {getCredentialDisplayName(credential, displayName)}
</Text> </Text>
<Text <Text
variant="large" variant="large"
className="relative top-1 font-mono tracking-tight" className="relative top-1 hidden flex-[0_0_40%] overflow-hidden truncate font-mono tracking-tight md:block"
> >
{"*".repeat(MASKED_KEY_LENGTH)} {"*".repeat(MASKED_KEY_LENGTH)}
</Text> </Text>

View File

@@ -9,6 +9,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { FileInput } from "@/components/atoms/FileInput/FileInput"; import { FileInput } from "@/components/atoms/FileInput/FileInput";
import { Switch } from "@/components/atoms/Switch/Switch"; import { Switch } from "@/components/atoms/Switch/Switch";
import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput"; import { GoogleDrivePickerInput } from "@/components/contextual/GoogleDrivePicker/GoogleDrivePickerInput";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { TimePicker } from "@/components/molecules/TimePicker/TimePicker"; import { TimePicker } from "@/components/molecules/TimePicker/TimePicker";
import { import {
BlockIOObjectSubSchema, BlockIOObjectSubSchema,
@@ -32,6 +33,7 @@ interface Props {
value?: any; value?: any;
placeholder?: string; placeholder?: string;
onChange: (value: any) => void; onChange: (value: any) => void;
readOnly?: boolean;
} }
/** /**
@@ -44,6 +46,7 @@ export function RunAgentInputs({
value, value,
placeholder, placeholder,
onChange, onChange,
readOnly = false,
...props ...props
}: Props & React.HTMLAttributes<HTMLElement>) { }: Props & React.HTMLAttributes<HTMLElement>) {
const { handleUploadFile, uploadProgress } = useRunAgentInputs(); const { handleUploadFile, uploadProgress } = useRunAgentInputs();
@@ -62,7 +65,6 @@ export function RunAgentInputs({
id={`${baseId}-number`} id={`${baseId}-number`}
label={schema.title ?? placeholder ?? "Number"} label={schema.title ?? placeholder ?? "Number"}
hideLabel hideLabel
size="small"
type="number" type="number"
value={value ?? ""} value={value ?? ""}
placeholder={placeholder || "Enter number"} placeholder={placeholder || "Enter number"}
@@ -80,7 +82,6 @@ export function RunAgentInputs({
id={`${baseId}-textarea`} id={`${baseId}-textarea`}
label={schema.title ?? placeholder ?? "Text"} label={schema.title ?? placeholder ?? "Text"}
hideLabel hideLabel
size="small"
type="textarea" type="textarea"
rows={3} rows={3}
value={value ?? ""} value={value ?? ""}
@@ -102,7 +103,7 @@ export function RunAgentInputs({
value={value} value={value}
onChange={onChange} onChange={onChange}
className="w-full" className="w-full"
showRemoveButton={false} showRemoveButton={!readOnly}
/> />
); );
break; break;
@@ -130,7 +131,6 @@ export function RunAgentInputs({
id={`${baseId}-date`} id={`${baseId}-date`}
label={schema.title ?? placeholder ?? "Date"} label={schema.title ?? placeholder ?? "Date"}
hideLabel hideLabel
size="small"
type="date" type="date"
value={value ? format(value as Date, "yyyy-MM-dd") : ""} value={value ? format(value as Date, "yyyy-MM-dd") : ""}
onChange={(e) => { onChange={(e) => {
@@ -159,7 +159,6 @@ export function RunAgentInputs({
id={`${baseId}-datetime`} id={`${baseId}-datetime`}
label={schema.title ?? placeholder ?? "Date time"} label={schema.title ?? placeholder ?? "Date time"}
hideLabel hideLabel
size="small"
type="datetime-local" type="datetime-local"
value={value ?? ""} value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)} onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -194,7 +193,6 @@ export function RunAgentInputs({
label={schema.title ?? placeholder ?? "Select"} label={schema.title ?? placeholder ?? "Select"}
hideLabel hideLabel
value={value ?? ""} value={value ?? ""}
size="small"
onValueChange={(val: string) => onChange(val)} onValueChange={(val: string) => onChange(val)}
placeholder={placeholder || "Select an option"} placeholder={placeholder || "Select an option"}
options={schema.enum options={schema.enum
@@ -217,7 +215,6 @@ export function RunAgentInputs({
items={allKeys.map((key) => ({ items={allKeys.map((key) => ({
value: key, value: key,
label: _schema.properties[key]?.title ?? key, label: _schema.properties[key]?.title ?? key,
size: "small",
}))} }))}
selectedValues={selectedValues} selectedValues={selectedValues}
onChange={(values: string[]) => onChange={(values: string[]) =>
@@ -336,7 +333,6 @@ export function RunAgentInputs({
id={`${baseId}-text`} id={`${baseId}-text`}
label={schema.title ?? placeholder ?? "Text"} label={schema.title ?? placeholder ?? "Text"}
hideLabel hideLabel
size="small"
type="text" type="text"
value={value ?? ""} value={value ?? ""}
onChange={(e) => onChange((e.target as HTMLInputElement).value)} onChange={(e) => onChange((e.target as HTMLInputElement).value)}
@@ -347,6 +343,17 @@ export function RunAgentInputs({
} }
return ( return (
<div className="no-drag relative flex w-full">{innerInputElement}</div> <div className="flex w-full flex-col gap-0 space-y-2">
<label className="large-medium flex items-center gap-1 font-medium">
{schema.title || placeholder}
<InformationTooltip description={schema.description} />
</label>
<div
className="no-drag relative flex w-full"
style={readOnly ? { pointerEvents: "none", opacity: 0.7 } : undefined}
>
{innerInputElement}
</div>
</div>
); );
} }

View File

@@ -73,22 +73,15 @@ export function ModalRunSection() {
title="Task Inputs" title="Task Inputs"
subtitle="Enter the information you want to provide to the agent for this task" subtitle="Enter the information you want to provide to the agent for this task"
> >
{/* Regular inputs */}
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div key={key} className="flex w-full flex-col gap-0 space-y-2"> <RunAgentInputs
<label className="flex items-center gap-1 text-sm font-medium"> key={key}
{inputSubSchema.title || key} schema={inputSubSchema}
<InformationTooltip description={inputSubSchema.description} /> value={inputValues[key] ?? inputSubSchema.default}
</label> placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
<RunAgentInputs data-testid={`agent-input-${key}`}
schema={inputSubSchema} />
value={inputValues[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
data-testid={`agent-input-${key}`}
/>
</div>
))} ))}
</ModalSection> </ModalSection>
) : null} ) : null}

View File

@@ -4,20 +4,19 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
ScrollableTabs,
ScrollableTabsContent,
ScrollableTabsList,
ScrollableTabsTrigger,
} from "@/components/molecules/ScrollableTabs/ScrollableTabs";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList"; import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews"; import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { InfoIcon } from "@phosphor-icons/react";
import { useEffect } from "react"; import { useEffect } from "react";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly"; import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { AnchorLinksWrap } from "../AnchorLinksWrap";
import { LoadingSelectedContent } from "../LoadingSelectedContent"; import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader"; import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
@@ -28,14 +27,12 @@ import { SelectedRunActions } from "./components/SelectedRunActions/SelectedRunA
import { WebhookTriggerSection } from "./components/WebhookTriggerSection"; import { WebhookTriggerSection } from "./components/WebhookTriggerSection";
import { useSelectedRunView } from "./useSelectedRunView"; import { useSelectedRunView } from "./useSelectedRunView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props { interface Props {
agent: LibraryAgent; agent: LibraryAgent;
runId: string; runId: string;
onSelectRun?: (id: string) => void; onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void; onClearSelectedRun?: () => void;
banner?: React.ReactNode;
} }
export function SelectedRunView({ export function SelectedRunView({
@@ -43,6 +40,7 @@ export function SelectedRunView({
runId, runId,
onSelectRun, onSelectRun,
onClearSelectedRun, onClearSelectedRun,
banner,
}: Props) { }: Props) {
const { run, preset, isLoading, responseError, httpError } = const { run, preset, isLoading, responseError, httpError } =
useSelectedRunView(agent.graph_id, runId); useSelectedRunView(agent.graph_id, runId);
@@ -65,13 +63,6 @@ export function SelectedRunView({
const withSummary = run?.stats?.activity_status; const withSummary = run?.stats?.activity_status;
const withReviews = run?.status === AgentExecutionStatus.REVIEW; const withReviews = run?.status === AgentExecutionStatus.REVIEW;
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
if (responseError || httpError) { if (responseError || httpError) {
return ( return (
<ErrorCard <ErrorCard
@@ -89,7 +80,11 @@ export function SelectedRunView({
return ( return (
<div className="flex h-full w-full gap-4"> <div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col"> <div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}> <SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={banner}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} /> <RunDetailHeader agent={agent} run={run} />
@@ -112,118 +107,116 @@ export function SelectedRunView({
/> />
)} )}
{/* Navigation Links */} <ScrollableTabs
<AnchorLinksWrap> defaultValue="output"
{withSummary && ( className="-mt-2 flex flex-col"
<button >
onClick={() => scrollToSection("summary")} <ScrollableTabsList className="px-4">
className={anchorStyles} {withSummary && (
> <ScrollableTabsTrigger value="summary">
Summary Summary
</button> </ScrollableTabsTrigger>
)}
<button
onClick={() => scrollToSection("output")}
className={anchorStyles}
>
Output
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
{withReviews && (
<button
onClick={() => scrollToSection("reviews")}
className={anchorStyles}
>
Reviews ({pendingReviews.length})
</button>
)}
</AnchorLinksWrap>
{/* Summary Section */}
{withSummary && (
<div id="summary" className="scroll-mt-4">
<RunDetailCard
title={
<div className="flex items-center gap-2">
<Text variant="lead-semibold">Summary</Text>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
size={16}
className="cursor-help text-neutral-500 hover:text-neutral-700"
/>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
This AI-generated summary describes how the agent
handled your task. It&apos;s an experimental
feature and may occasionally be inaccurate.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
}
>
<RunSummary run={run} />
</RunDetailCard>
</div>
)}
{/* Output Section */}
<div id="output" className="scroll-mt-4">
<RunDetailCard title="Output">
{isLoading ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<Text variant="body" className="text-neutral-600">
No output from this run.
</Text>
)} )}
</RunDetailCard> <ScrollableTabsTrigger value="output">
</div> Output
</ScrollableTabsTrigger>
{/* Input Section */} <ScrollableTabsTrigger value="input">
<div id="input" className="scroll-mt-4"> Your input
<RunDetailCard title="Your input"> </ScrollableTabsTrigger>
<AgentInputsReadOnly {withReviews && (
agent={agent} <ScrollableTabsTrigger value="reviews">
inputs={run?.inputs} Reviews ({pendingReviews.length})
credentialInputs={run?.credential_inputs} </ScrollableTabsTrigger>
/> )}
</RunDetailCard> </ScrollableTabsList>
</div> <div className="my-6 flex flex-col gap-6">
{/* Summary Section */}
{/* Reviews Section */} {withSummary && (
{withReviews && ( <ScrollableTabsContent value="summary">
<div id="reviews" className="scroll-mt-4"> <div className="scroll-mt-4">
<RunDetailCard> <RunDetailCard
{reviewsLoading ? ( title={
<div className="text-neutral-500">Loading reviews</div> <div className="flex items-center gap-1">
) : pendingReviews.length > 0 ? ( <Text variant="lead-semibold">Summary</Text>
<PendingReviewsList <InformationTooltip
reviews={pendingReviews} iconSize={20}
onReviewComplete={refetchReviews} description="This AI-generated summary describes how the agent handled your task. It's an experimental feature and may occasionally be inaccurate."
emptyMessage="No pending reviews for this execution" />
/> </div>
) : ( }
<div className="text-neutral-600"> >
No pending reviews for this execution <RunSummary run={run} />
</RunDetailCard>
</div> </div>
)} </ScrollableTabsContent>
</RunDetailCard> )}
{/* Output Section */}
<ScrollableTabsContent value="output">
<div className="scroll-mt-4">
<RunDetailCard title="Output">
{isLoading ? (
<div className="text-neutral-500">
<LoadingSpinner />
</div>
) : run && "outputs" in run ? (
<RunOutputs outputs={run.outputs as any} />
) : (
<Text variant="body" className="text-neutral-600">
No output from this run.
</Text>
)}
</RunDetailCard>
</div>
</ScrollableTabsContent>
{/* Input Section */}
<ScrollableTabsContent value="input">
<div id="input" className="scroll-mt-4">
<RunDetailCard
title={
<div className="flex items-center gap-1">
<Text variant="lead-semibold">Your input</Text>
<InformationTooltip
iconSize={20}
description="This is the input that was provided to the agent for running this task."
/>
</div>
}
>
<AgentInputsReadOnly
agent={agent}
inputs={run?.inputs}
credentialInputs={run?.credential_inputs}
/>
</RunDetailCard>
</div>
</ScrollableTabsContent>
{/* Reviews Section */}
{withReviews && (
<ScrollableTabsContent value="reviews">
<div className="scroll-mt-4">
<RunDetailCard>
{reviewsLoading ? (
<LoadingSpinner size="small" />
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<Text variant="body" className="text-zinc-700">
No pending reviews for this execution
</Text>
)}
</RunDetailCard>
</div>
</ScrollableTabsContent>
)}
</div> </div>
)} </ScrollableTabs>
</div> </div>
</SelectedViewLayout> </SelectedViewLayout>
</div> </div>

View File

@@ -9,7 +9,6 @@ import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils"; import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly"; import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { AnchorLinksWrap } from "../AnchorLinksWrap";
import { LoadingSelectedContent } from "../LoadingSelectedContent"; import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader"; import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
@@ -17,19 +16,18 @@ import { SelectedViewLayout } from "../SelectedViewLayout";
import { SelectedScheduleActions } from "./components/SelectedScheduleActions"; import { SelectedScheduleActions } from "./components/SelectedScheduleActions";
import { useSelectedScheduleView } from "./useSelectedScheduleView"; import { useSelectedScheduleView } from "./useSelectedScheduleView";
const anchorStyles =
"border-b-2 border-transparent pb-1 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 hover:border-slate-900";
interface Props { interface Props {
agent: LibraryAgent; agent: LibraryAgent;
scheduleId: string; scheduleId: string;
onClearSelectedRun?: () => void; onClearSelectedRun?: () => void;
banner?: React.ReactNode;
} }
export function SelectedScheduleView({ export function SelectedScheduleView({
agent, agent,
scheduleId, scheduleId,
onClearSelectedRun, onClearSelectedRun,
banner,
}: Props) { }: Props) {
const { schedule, isLoading, error } = useSelectedScheduleView( const { schedule, isLoading, error } = useSelectedScheduleView(
agent.graph_id, agent.graph_id,
@@ -45,13 +43,6 @@ export function SelectedScheduleView({
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint); const isLgScreenUp = isLargeScreen(breakpoint);
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
if (error) { if (error) {
return ( return (
<ErrorCard <ErrorCard
@@ -85,7 +76,11 @@ export function SelectedScheduleView({
return ( return (
<div className="flex h-full w-full gap-4"> <div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col"> <div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}> <SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={banner}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex w-full flex-col gap-0"> <div className="flex w-full flex-col gap-0">
<RunDetailHeader <RunDetailHeader
@@ -108,22 +103,6 @@ export function SelectedScheduleView({
) : null} ) : null}
</div> </div>
{/* Navigation Links */}
<AnchorLinksWrap>
<button
onClick={() => scrollToSection("schedule")}
className={anchorStyles}
>
Schedule
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
</AnchorLinksWrap>
{/* Schedule Section */} {/* Schedule Section */}
<div id="schedule" className="scroll-mt-4"> <div id="schedule" className="scroll-mt-4">
<RunDetailCard title="Schedule"> <RunDetailCard title="Schedule">

View File

@@ -1,84 +0,0 @@
"use client";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { PencilSimpleIcon } from "@phosphor-icons/react";
import { RunAgentInputs } from "../../../../modals/RunAgentInputs/RunAgentInputs";
import { useEditInputsModal } from "./useEditInputsModal";
type Props = {
agent: LibraryAgent;
schedule: GraphExecutionJobInfo;
};
export function EditInputsModal({ agent, schedule }: Props) {
const {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} = useEditInputsModal(agent, schedule);
return (
<Dialog
controlled={{ isOpen, set: setIsOpen }}
styling={{ maxWidth: "32rem" }}
>
<Dialog.Trigger>
<Button
variant="ghost"
size="small"
className="absolute -right-2 -top-2"
>
<PencilSimpleIcon className="size-4" /> Edit inputs
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="h3">Edit inputs</Text>
<div className="flex flex-col gap-4">
{Object.entries(inputFields).map(([key, fieldSchema]) => (
<div key={key} className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{fieldSchema?.title || key}
</label>
<RunAgentInputs
schema={fieldSchema as any}
value={values[key]}
onChange={(v) => setValues((prev) => ({ ...prev, [key]: v }))}
/>
</div>
))}
</div>
</div>
<Dialog.Footer>
<div className="flex w-full justify-end gap-2">
<Button
variant="secondary"
size="small"
onClick={() => setIsOpen(false)}
className="min-w-32"
>
Cancel
</Button>
<Button
variant="primary"
size="small"
onClick={handleSave}
loading={isSaving}
className="min-w-32"
>
{isSaving ? "Saving…" : "Save"}
</Button>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,78 +0,0 @@
"use client";
import { useMemo, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getGetV1ListExecutionSchedulesForAGraphQueryKey } from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { useToast } from "@/components/molecules/Toast/use-toast";
function getAgentInputFields(agent: LibraryAgent): Record<string, any> {
const schema = agent.input_schema as unknown as {
properties?: Record<string, any>;
} | null;
if (!schema || !schema.properties) return {};
const properties = schema.properties as Record<string, any>;
const visibleEntries = Object.entries(properties).filter(
([, sub]) => !sub?.hidden,
);
return Object.fromEntries(visibleEntries);
}
export function useEditInputsModal(
agent: LibraryAgent,
schedule: GraphExecutionJobInfo,
) {
const queryClient = useQueryClient();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const inputFields = useMemo(() => getAgentInputFields(agent), [agent]);
const [values, setValues] = useState<Record<string, any>>({
...(schedule.input_data as Record<string, any>),
});
async function handleSave() {
setIsSaving(true);
try {
const res = await fetch(`/api/schedules/${schedule.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inputs: values }),
});
if (!res.ok) {
let message = "Failed to update schedule inputs";
const data = await res.json();
message = data?.message || data?.detail || message;
throw new Error(message);
}
await queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryKey(
schedule.graph_id,
),
});
toast({
title: "Schedule inputs updated",
});
setIsOpen(false);
} catch (error: any) {
toast({
title: "Failed to update schedule inputs",
description: error?.message || "An unexpected error occurred.",
variant: "destructive",
});
}
setIsSaving(false);
}
return {
isOpen,
setIsOpen,
inputFields,
values,
setValues,
handleSave,
isSaving,
} as const;
}

View File

@@ -25,9 +25,10 @@ export function SelectedScheduleActions({ agent, scheduleId }: Props) {
<Button <Button
variant="icon" variant="icon"
size="icon" size="icon"
aria-label="Open in builder"
as="NextLink" as="NextLink"
href={openInBuilderHref} href={openInBuilderHref}
target="_blank"
aria-label="View scheduled task details"
> >
<EyeIcon weight="bold" size={18} className="text-zinc-700" /> <EyeIcon weight="bold" size={18} className="text-zinc-700" />
</Button> </Button>

View File

@@ -4,7 +4,6 @@ import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExe
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input"; import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { import {
getAgentCredentialsFields, getAgentCredentialsFields,
getAgentInputFields, getAgentInputFields,
@@ -25,6 +24,7 @@ interface Props {
onClearSelectedRun?: () => void; onClearSelectedRun?: () => void;
onRunCreated?: (execution: GraphExecutionMeta) => void; onRunCreated?: (execution: GraphExecutionMeta) => void;
onSwitchToRunsTab?: () => void; onSwitchToRunsTab?: () => void;
banner?: React.ReactNode;
} }
export function SelectedTemplateView({ export function SelectedTemplateView({
@@ -33,6 +33,7 @@ export function SelectedTemplateView({
onClearSelectedRun, onClearSelectedRun,
onRunCreated, onRunCreated,
onSwitchToRunsTab, onSwitchToRunsTab,
banner,
}: Props) { }: Props) {
const { const {
template, template,
@@ -101,7 +102,11 @@ export function SelectedTemplateView({
return ( return (
<div className="flex h-full w-full gap-4"> <div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col"> <div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}> <SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={banner}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} /> <RunDetailHeader agent={agent} run={undefined} />
@@ -138,25 +143,13 @@ export function SelectedTemplateView({
<RunDetailCard title="Your Input"> <RunDetailCard title="Your Input">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div <RunAgentInputs
key={key} key={key}
className="flex w-full flex-col gap-0 space-y-2" schema={inputSubSchema}
> value={inputs[key] ?? inputSubSchema.default}
<label className="flex items-center gap-1 text-sm font-medium"> placeholder={inputSubSchema.description}
{inputSubSchema.title || key} onChange={(value) => setInputValue(key, value)}
{inputSubSchema.description && ( />
<InformationTooltip
description={inputSubSchema.description}
/>
)}
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
</div>
))} ))}
</div> </div>
</RunDetailCard> </RunDetailCard>

View File

@@ -3,7 +3,6 @@
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Input } from "@/components/atoms/Input/Input"; import { Input } from "@/components/atoms/Input/Input";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { import {
getAgentCredentialsFields, getAgentCredentialsFields,
getAgentInputFields, getAgentInputFields,
@@ -23,6 +22,7 @@ interface Props {
triggerId: string; triggerId: string;
onClearSelectedRun?: () => void; onClearSelectedRun?: () => void;
onSwitchToRunsTab?: () => void; onSwitchToRunsTab?: () => void;
banner?: React.ReactNode;
} }
export function SelectedTriggerView({ export function SelectedTriggerView({
@@ -30,6 +30,7 @@ export function SelectedTriggerView({
triggerId, triggerId,
onClearSelectedRun, onClearSelectedRun,
onSwitchToRunsTab, onSwitchToRunsTab,
banner,
}: Props) { }: Props) {
const { const {
trigger, trigger,
@@ -94,7 +95,11 @@ export function SelectedTriggerView({
return ( return (
<div className="flex h-full w-full gap-4"> <div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col"> <div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}> <SelectedViewLayout
agentName={agent.name}
agentId={agent.id}
banner={banner}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} /> <RunDetailHeader agent={agent} run={undefined} />
@@ -131,25 +136,13 @@ export function SelectedTriggerView({
<RunDetailCard title="Your Input"> <RunDetailCard title="Your Input">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{inputFields.map(([key, inputSubSchema]) => ( {inputFields.map(([key, inputSubSchema]) => (
<div <RunAgentInputs
key={key} key={key}
className="flex w-full flex-col gap-0 space-y-2" schema={inputSubSchema}
> value={inputs[key] ?? inputSubSchema.default}
<label className="flex items-center gap-1 text-sm font-medium"> placeholder={inputSubSchema.description}
{inputSubSchema.title || key} onChange={(value) => setInputValue(key, value)}
{inputSubSchema.description && ( />
<InformationTooltip
description={inputSubSchema.description}
/>
)}
</label>
<RunAgentInputs
schema={inputSubSchema}
value={inputs[key] ?? inputSubSchema.default}
placeholder={inputSubSchema.description}
onChange={(value) => setInputValue(key, value)}
/>
</div>
))} ))}
</div> </div>
</RunDetailCard> </RunDetailCard>

View File

@@ -6,6 +6,7 @@ interface Props {
agentName: string; agentName: string;
agentId: string; agentId: string;
children: React.ReactNode; children: React.ReactNode;
banner?: React.ReactNode;
} }
export function SelectedViewLayout(props: Props) { export function SelectedViewLayout(props: Props) {
@@ -14,10 +15,15 @@ export function SelectedViewLayout(props: Props) {
<div <div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`} className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
> >
{props.banner && <div className="mb-4">{props.banner}</div>}
<Breadcrumbs <Breadcrumbs
items={[ items={[
{ name: "My Library", link: "/library" }, { name: "My Library", link: "/library" },
{ name: props.agentName, link: `/library/agents/${props.agentId}` }, {
name: props.agentName,
link: `/library/agents/${props.agentId}`,
testId: "agent-title"
},
]} ]}
/> />
</div> </div>

View File

@@ -0,0 +1,163 @@
import {
useGetV2GetSpecificAgent,
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import {
usePatchV2UpdateLibraryAgent,
getGetV2GetLibraryAgentQueryKey,
} from "@/app/api/__generated__/endpoints/library/library";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useQueryClient } from "@tanstack/react-query";
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
import { okData } from "@/app/api/helpers";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import * as React from "react";
import { useState } from "react";
interface UseMarketplaceUpdateProps {
agent: LibraryAgent | null | undefined;
}
export function useMarketplaceUpdate({ agent }: UseMarketplaceUpdateProps) {
const [modalOpen, setModalOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
const user = useSupabaseStore((state) => state.user);
// Get marketplace data if agent has marketplace listing
const { data: storeAgentData } = useGetV2GetSpecificAgent(
agent?.marketplace_listing?.creator.slug || "",
agent?.marketplace_listing?.slug || "",
{},
{
query: {
enabled: !!(
agent?.marketplace_listing?.creator.slug &&
agent?.marketplace_listing?.slug
),
},
},
);
// Get user's submissions to check for pending submissions
const { data: submissionsData } = useGetV2ListMySubmissions(
{ page: 1, page_size: 50 }, // Get enough to cover recent submissions
{
query: {
enabled: !!user?.id, // Only fetch if user is authenticated
},
},
);
const updateToLatestMutation = usePatchV2UpdateLibraryAgent({
mutation: {
onError: (err) => {
toast({
title: "Update Failed",
description: "Failed to update agent to latest version",
variant: "destructive",
});
console.error("Failed to update agent:", err);
},
onSuccess: () => {
toast({
title: "Agent Updated",
description: "Agent updated to latest version successfully",
});
// Invalidate to get the updated agent data from the server
if (agent?.id) {
queryClient.invalidateQueries({
queryKey: getGetV2GetLibraryAgentQueryKey(agent.id),
});
}
},
},
});
// Check if marketplace has a newer version than user's current version
const marketplaceUpdateInfo = React.useMemo(() => {
const storeAgent = okData(storeAgentData) as any;
if (!agent || !storeAgent) {
return {
hasUpdate: false,
latestVersion: undefined,
isUserCreator: false,
};
}
// Get the latest version from the marketplace
// agentGraphVersions array contains graph version numbers as strings, get the highest one
const latestMarketplaceVersion =
storeAgent.agentGraphVersions?.length > 0
? Math.max(
...storeAgent.agentGraphVersions.map((v: string) =>
parseInt(v, 10),
),
)
: undefined;
// Determine if the user is the creator of this agent
// Compare current user ID with the marketplace listing creator ID
const isUserCreator =
user?.id && agent.marketplace_listing?.creator.id === user.id;
// Check if there's a pending submission for this specific agent version
const submissionsResponse = okData(submissionsData) as any;
const hasPendingSubmissionForCurrentVersion =
isUserCreator &&
submissionsResponse?.submissions?.some(
(submission: StoreSubmission) =>
submission.agent_id === agent.graph_id &&
submission.agent_version === agent.graph_version &&
submission.status === "PENDING",
);
// If user is creator and their version is newer than marketplace, show publish update banner
// BUT only if there's no pending submission for this version
const hasPublishUpdate =
isUserCreator &&
!hasPendingSubmissionForCurrentVersion &&
latestMarketplaceVersion !== undefined &&
agent.graph_version > latestMarketplaceVersion;
// If marketplace version is newer than user's version, show update banner
// This applies to both creators and non-creators
const hasMarketplaceUpdate =
latestMarketplaceVersion !== undefined &&
latestMarketplaceVersion > agent.graph_version;
return {
hasUpdate: hasMarketplaceUpdate,
latestVersion: latestMarketplaceVersion,
isUserCreator,
hasPublishUpdate,
};
}, [agent, storeAgentData, user, submissionsData]);
const handlePublishUpdate = () => {
setModalOpen(true);
};
const handleUpdateToLatest = () => {
if (!agent || marketplaceUpdateInfo.latestVersion === undefined) return;
// Update to the specific marketplace version using the new graph_version parameter
updateToLatestMutation.mutate({
libraryAgentId: agent.id,
data: {
graph_version: marketplaceUpdateInfo.latestVersion,
},
});
};
return {
hasAgentMarketplaceUpdate: marketplaceUpdateInfo.hasPublishUpdate,
hasMarketplaceUpdate: marketplaceUpdateInfo.hasUpdate,
latestMarketplaceVersion: marketplaceUpdateInfo.latestVersion,
isUpdating: updateToLatestMutation.isPending,
modalOpen,
setModalOpen,
handlePublishUpdate,
handleUpdateToLatest,
};
}

View File

@@ -680,28 +680,20 @@ export function AgentRunDraftView({
{/* Regular inputs */} {/* Regular inputs */}
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => ( {Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
<div key={key} className="flex flex-col space-y-2"> <RunAgentInputs
<label className="flex items-center gap-1 text-sm font-medium"> key={key}
{inputSubSchema.title || key} schema={inputSubSchema}
<InformationTooltip value={inputValues[key] ?? inputSubSchema.default}
description={inputSubSchema.description} placeholder={inputSubSchema.description}
/> onChange={(value) => {
</label> setInputValues((obj) => ({
...obj,
<RunAgentInputs [key]: value,
schema={inputSubSchema} }));
value={inputValues[key] ?? inputSubSchema.default} setChangedPresetAttributes((prev) => prev.add("inputs"));
placeholder={inputSubSchema.description} }}
onChange={(value) => { data-testid={`agent-input-${key}`}
setInputValues((obj) => ({ />
...obj,
[key]: value,
}));
setChangedPresetAttributes((prev) => prev.add("inputs"));
}}
data-testid={`agent-input-${key}`}
/>
</div>
))} ))}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -5,7 +5,20 @@ import { Separator } from "@/components/__legacy__/ui/separator";
import Link from "next/link"; import Link from "next/link";
import { User } from "@supabase/supabase-js"; import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import type { ChangelogEntry } from "@/app/api/__generated__/models/changelogEntry";
import { useAgentInfo } from "./useAgentInfo"; import { useAgentInfo } from "./useAgentInfo";
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import { Text } from "@/components/atoms/Text/Text";
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
import * as React from "react";
import { MarketplaceBanners } from "../../../components/MarketplaceBanners/MarketplaceBanners";
import {
getLatestMarketplaceVersion,
isUserCreator as checkIsUserCreator,
calculateUpdateStatus,
} from "@/components/contextual/marketplaceHelpers";
interface AgentInfoProps { interface AgentInfoProps {
user: User | null; user: User | null;
@@ -21,6 +34,8 @@ interface AgentInfoProps {
version: string; version: string;
storeListingVersionId: string; storeListingVersionId: string;
isAgentAddedToLibrary: boolean; isAgentAddedToLibrary: boolean;
creatorSlug?: string;
agentSlug?: string;
} }
export const AgentInfo = ({ export const AgentInfo = ({
@@ -37,6 +52,8 @@ export const AgentInfo = ({
version, version,
storeListingVersionId, storeListingVersionId,
isAgentAddedToLibrary, isAgentAddedToLibrary,
creatorSlug,
agentSlug,
}: AgentInfoProps) => { }: AgentInfoProps) => {
const { const {
handleDownload, handleDownload,
@@ -45,6 +62,137 @@ export const AgentInfo = ({
isAddingAgentToLibrary, isAddingAgentToLibrary,
} = useAgentInfo({ storeListingVersionId }); } = useAgentInfo({ storeListingVersionId });
// Get current user for update detection
const currentUser = useSupabaseStore((state) => state.user);
// State for expanding version list - start with 3, then show 3 more each time
const [visibleVersionCount, setVisibleVersionCount] = React.useState(3);
// Get store agent data for version history
const { data: storeAgentData } = useGetV2GetSpecificAgent(
creatorSlug || "",
agentSlug || "",
{ include_changelog: true },
{
query: {
enabled: !!(creatorSlug && agentSlug),
},
},
);
// Calculate update information using simple helper functions
const storeData = okData<StoreAgentDetails>(storeAgentData);
const latestMarketplaceVersion = getLatestMarketplaceVersion(
storeData?.agentGraphVersions,
);
const currentVersion = parseInt(version, 10);
const isCreator = checkIsUserCreator(creator, currentUser);
const updateStatus = calculateUpdateStatus({
latestMarketplaceVersion,
currentVersion,
isUserCreator: isCreator,
isAgentAddedToLibrary,
});
const updateInfo = {
...updateStatus,
latestVersion: latestMarketplaceVersion,
isUserCreator: isCreator,
};
// Process version data for display - use store listing versions (not agentGraphVersions)
const allVersions = storeData?.versions
? storeData.versions
.map((versionStr: string) => parseInt(versionStr, 10))
.sort((a: number, b: number) => b - a)
.map((versionNum: number) => ({
version: versionNum,
isCurrentVersion: false, // We'll update this logic if needed
}))
: [];
const agentVersions = allVersions.slice(0, visibleVersionCount);
const hasMoreVersions = allVersions.length > visibleVersionCount;
const formatDate = (version: number) => {
// Generate sample dates based on version
const baseDate = new Date("2025-12-18");
baseDate.setDate(baseDate.getDate() - (19 - version) * 7);
return baseDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const renderVersionItem = (versionInfo: {
version: number;
isCurrentVersion: boolean;
}) => {
// Find real changelog data for this version
const storeData = okData<StoreAgentDetails>(storeAgentData);
const changelogEntry = storeData?.changelog?.find(
(entry: ChangelogEntry) =>
entry.version === versionInfo.version.toString(),
);
return (
<div key={versionInfo.version} className="mb-6 last:mb-0">
{/* Version Header */}
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Text
variant="body"
className="font-semibold text-neutral-900 dark:text-neutral-100"
>
Version {versionInfo.version}.0
</Text>
{versionInfo.isCurrentVersion && (
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
Current
</span>
)}
</div>
<Text
variant="small"
className="text-neutral-500 dark:text-neutral-400"
>
{changelogEntry
? new Date(changelogEntry.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: formatDate(versionInfo.version)}
</Text>
</div>
{/* Real Changelog Content */}
{changelogEntry && (
<div className="space-y-2">
<Text
variant="body"
className="text-neutral-700 dark:text-neutral-300"
>
{changelogEntry.changes_summary}
</Text>
</div>
)}
</div>
);
};
const renderMarketplaceBanners = () => {
return (
<MarketplaceBanners
hasUpdate={updateInfo.hasUpdate}
latestVersion={updateInfo.latestVersion}
hasUnpublishedChanges={updateInfo.hasUnpublishedChanges}
currentVersion={parseInt(version, 10)}
/>
);
};
return ( return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0"> <div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */} {/* Title */}
@@ -158,17 +306,51 @@ export const AgentInfo = ({
</div> </div>
</div> </div>
{/* Version History */} {/* Update/Unpublished Changes Banners */}
<div className="flex w-full flex-col gap-0.5 sm:gap-1"> {renderMarketplaceBanners()}
{/* Changelog */}
<div className="flex w-full flex-col gap-1.5 sm:gap-2">
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2"> <div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
Version history Changelog
</div> </div>
<div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400"> <div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
Last updated {lastUpdated} Last updated {lastUpdated}
</div> </div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version} {/* Version List */}
</div> {agentVersions.length > 0 ? (
<div className="mt-4">
{agentVersions.map(renderVersionItem)}
{hasMoreVersions && (
<button
onClick={() => setVisibleVersionCount((prev) => prev + 3)}
className="mt-2 flex items-center gap-1 text-sm font-medium text-neutral-900 hover:text-neutral-700 dark:text-neutral-100 dark:hover:text-neutral-300"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M4 6l4 4 4-4"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Read more</span>
</button>
)}
</div>
) : (
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,8 @@
import { Separator } from "@/components/__legacy__/ui/separator"; import { Separator } from "@/components/__legacy__/ui/separator";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs"; import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page"; import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
import { AgentImages } from "../AgentImages/AgentImage"; import { AgentImages } from "../AgentImages/AgentImage";
import { AgentInfo } from "../AgentInfo/AgentInfo"; import { AgentInfo } from "../AgentInfo/AgentInfo";
@@ -46,7 +48,8 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
); );
} }
if (!agent) { const agentData = okData<StoreAgentDetails>(agent);
if (!agentData) {
return ( return (
<div className="mx-auto w-full max-w-[1360px]"> <div className="mx-auto w-full max-w-[1360px]">
<main className="px-4"> <main className="px-4">
@@ -67,10 +70,10 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
const breadcrumbs = [ const breadcrumbs = [
{ name: "Marketplace", link: "/marketplace" }, { name: "Marketplace", link: "/marketplace" },
{ {
name: agent.creator, name: agentData.creator ?? "",
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`, link: `/marketplace/creator/${encodeURIComponent(agentData.creator ?? "")}`,
}, },
{ name: agent.agent_name, link: "#" }, { name: agentData.agent_name ?? "", link: "#" },
]; ];
return ( return (
@@ -82,18 +85,29 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
<div className="w-full md:w-auto md:shrink-0"> <div className="w-full md:w-auto md:shrink-0">
<AgentInfo <AgentInfo
user={user} user={user}
agentId={agent.active_version_id ?? ""} agentId={agentData.active_version_id ?? ""}
name={agent.agent_name} name={agentData.agent_name ?? ""}
creator={agent.creator} creator={agentData.creator ?? ""}
shortDescription={agent.sub_heading} shortDescription={agentData.sub_heading ?? ""}
longDescription={agent.description} longDescription={agentData.description ?? ""}
rating={agent.rating} rating={agentData.rating ?? 0}
runs={agent.runs} runs={agentData.runs ?? 0}
categories={agent.categories} categories={agentData.categories ?? []}
lastUpdated={agent.last_updated.toISOString()} lastUpdated={
version={agent.versions[agent.versions.length - 1]} agentData.last_updated?.toISOString() ??
storeListingVersionId={agent.store_listing_version_id} new Date().toISOString()
}
version={
agentData.versions
? Math.max(
...agentData.versions.map((v: string) => parseInt(v, 10)),
).toString()
: "1"
}
storeListingVersionId={agentData.store_listing_version_id ?? ""}
isAgentAddedToLibrary={Boolean(libraryAgent)} isAgentAddedToLibrary={Boolean(libraryAgent)}
creatorSlug={params.creator}
agentSlug={params.slug}
/> />
</div> </div>
<AgentImages <AgentImages
@@ -101,23 +115,23 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
const orderedImages: string[] = []; const orderedImages: string[] = [];
// 1. YouTube/Overview video (if it exists) // 1. YouTube/Overview video (if it exists)
if (agent.agent_video) { if (agentData.agent_video) {
orderedImages.push(agent.agent_video); orderedImages.push(agentData.agent_video);
} }
// 2. First image (hero) // 2. First image (hero)
if (agent.agent_image.length > 0) { if (agentData.agent_image?.length > 0) {
orderedImages.push(agent.agent_image[0]); orderedImages.push(agentData.agent_image[0]);
} }
// 3. Agent Output Demo (if it exists) // 3. Agent Output Demo (if it exists)
if ((agent as any).agent_output_demo) { if (agentData.agent_output_demo) {
orderedImages.push((agent as any).agent_output_demo); orderedImages.push(agentData.agent_output_demo);
} }
// 4. Additional images // 4. Additional images
if (agent.agent_image.length > 1) { if (agentData.agent_image && agentData.agent_image.length > 1) {
orderedImages.push(...agent.agent_image.slice(1)); orderedImages.push(...agentData.agent_image.slice(1));
} }
return orderedImages; return orderedImages;
@@ -129,7 +143,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
<AgentsSection <AgentsSection
margin="32px" margin="32px"
agents={otherAgents.agents} agents={otherAgents.agents}
sectionTitle={`Other agents by ${agent.creator}`} sectionTitle={`Other agents by ${agentData.creator ?? ""}`}
/> />
)} )}
<Separator className="mb-[25px] mt-[60px]" /> <Separator className="mb-[25px] mt-[60px]" />

View File

@@ -7,6 +7,7 @@ import { useGetV2GetAgentByStoreId } from "@/app/api/__generated__/endpoints/lib
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse"; import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails"; import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export const useMainAgentPage = ({ export const useMainAgentPage = ({
@@ -20,13 +21,7 @@ export const useMainAgentPage = ({
data: agent, data: agent,
isLoading: isAgentLoading, isLoading: isAgentLoading,
isError: isAgentError, isError: isAgentError,
} = useGetV2GetSpecificAgent(creator_lower, params.slug, { } = useGetV2GetSpecificAgent(creator_lower, params.slug);
query: {
select: (x) => {
return x.data as StoreAgentDetails;
},
},
});
const { const {
data: otherAgents, data: otherAgents,
isLoading: isOtherAgentsLoading, isLoading: isOtherAgentsLoading,
@@ -59,14 +54,18 @@ export const useMainAgentPage = ({
data: libraryAgent, data: libraryAgent,
isLoading: isLibraryAgentLoading, isLoading: isLibraryAgentLoading,
isError: isLibraryAgentError, isError: isLibraryAgentError,
} = useGetV2GetAgentByStoreId(agent?.active_version_id ?? "", { } = useGetV2GetAgentByStoreId(
query: { okData<StoreAgentDetails>(agent)?.active_version_id ?? "",
select: (x) => { {
return x.data as LibraryAgent; query: {
select: (x) => {
return x.data as LibraryAgent;
},
enabled:
!!user && !!okData<StoreAgentDetails>(agent)?.active_version_id,
}, },
enabled: !!user && !!agent?.active_version_id,
}, },
}); );
const isLoading = const isLoading =
isAgentLoading || isAgentLoading ||

View File

@@ -2796,6 +2796,16 @@
"in": "path", "in": "path",
"required": true, "required": true,
"schema": { "type": "string", "title": "Agent Name" } "schema": { "type": "string", "title": "Agent Name" }
},
{
"name": "include_changelog",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": false,
"title": "Include Changelog"
}
} }
], ],
"responses": { "responses": {
@@ -5983,6 +5993,16 @@
"required": ["file"], "required": ["file"],
"title": "Body_postV2Upload submission media" "title": "Body_postV2Upload submission media"
}, },
"ChangelogEntry": {
"properties": {
"version": { "type": "string", "title": "Version" },
"changes_summary": { "type": "string", "title": "Changes Summary" },
"date": { "type": "string", "format": "date-time", "title": "Date" }
},
"type": "object",
"required": ["version", "changes_summary", "date"],
"title": "ChangelogEntry"
},
"ChatRequest": { "ChatRequest": {
"properties": { "properties": {
"query": { "type": "string", "title": "Query" }, "query": { "type": "string", "title": "Query" },
@@ -7426,6 +7446,11 @@
"title": "Auto Update Version", "title": "Auto Update Version",
"description": "Auto-update the agent version" "description": "Auto-update the agent version"
}, },
"graph_version": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Graph Version",
"description": "Specific graph version to update to"
},
"is_favorite": { "is_favorite": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }], "anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Is Favorite", "title": "Is Favorite",
@@ -8902,6 +8927,12 @@
"type": "array", "type": "array",
"title": "Versions" "title": "Versions"
}, },
"agentGraphVersions": {
"items": { "type": "string" },
"type": "array",
"title": "Agentgraphversions"
},
"agentGraphId": { "type": "string", "title": "Agentgraphid" },
"last_updated": { "last_updated": {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@@ -8919,6 +8950,16 @@
"type": "boolean", "type": "boolean",
"title": "Has Approved Version", "title": "Has Approved Version",
"default": false "default": false
},
"changelog": {
"anyOf": [
{
"items": { "$ref": "#/components/schemas/ChangelogEntry" },
"type": "array"
},
{ "type": "null" }
],
"title": "Changelog"
} }
}, },
"type": "object", "type": "object",
@@ -8937,6 +8978,8 @@
"runs", "runs",
"rating", "rating",
"versions", "versions",
"agentGraphVersions",
"agentGraphId",
"last_updated" "last_updated"
], ],
"title": "StoreAgentDetails" "title": "StoreAgentDetails"

View File

@@ -6,6 +6,10 @@ import {
import { environment } from "@/services/environment"; import { environment } from "@/services/environment";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
// Increase body size limit to 256MB to match backend file upload limit
export const maxDuration = 300; // 5 minutes timeout for large uploads
export const dynamic = "force-dynamic";
function buildBackendUrl(path: string[], queryString: string): string { function buildBackendUrl(path: string[], queryString: string): string {
const backendPath = path.join("/"); const backendPath = path.join("/");
return `${environment.getAGPTServerBaseUrl()}/${backendPath}${queryString}`; return `${environment.getAGPTServerBaseUrl()}/${backendPath}${queryString}`;

View File

@@ -1,36 +1,33 @@
"use client"; "use client";
import { LaunchDarklyProvider } from "@/services/feature-flags/feature-flag-provider"; import { TooltipProvider } from "@/components/atoms/Tooltip/BaseTooltip";
import OnboardingProvider from "@/providers/onboarding/onboarding-provider"; import { SentryUserTracker } from "@/components/monitor/SentryUserTracker";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context"; import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { getQueryClient } from "@/lib/react-query/queryClient"; import { getQueryClient } from "@/lib/react-query/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps,
} from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { TooltipProvider } from "@/components/atoms/Tooltip/BaseTooltip";
import CredentialsProvider from "@/providers/agent-credentials/credentials-provider"; import CredentialsProvider from "@/providers/agent-credentials/credentials-provider";
import { SentryUserTracker } from "@/components/monitor/SentryUserTracker"; import OnboardingProvider from "@/providers/onboarding/onboarding-provider";
import { LaunchDarklyProvider } from "@/services/feature-flags/feature-flag-provider";
import { QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider, ThemeProviderProps } from "next-themes";
import { NuqsAdapter } from "nuqs/adapters/next/app";
export function Providers({ children, ...props }: ThemeProviderProps) { export function Providers({ children, ...props }: ThemeProviderProps) {
const queryClient = getQueryClient(); const queryClient = getQueryClient();
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<NuqsAdapter> <NuqsAdapter>
<NextThemesProvider {...props}> <BackendAPIProvider>
<BackendAPIProvider> <SentryUserTracker />
<SentryUserTracker /> <CredentialsProvider>
<CredentialsProvider> <LaunchDarklyProvider>
<LaunchDarklyProvider> <OnboardingProvider>
<OnboardingProvider> <ThemeProvider forcedTheme="light" {...props}>
<TooltipProvider>{children}</TooltipProvider> <TooltipProvider>{children}</TooltipProvider>
</OnboardingProvider> </ThemeProvider>
</LaunchDarklyProvider> </OnboardingProvider>
</CredentialsProvider> </LaunchDarklyProvider>
</BackendAPIProvider> </CredentialsProvider>
</NextThemesProvider> </BackendAPIProvider>
</NuqsAdapter> </NuqsAdapter>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -0,0 +1,157 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { OverflowText } from "./OverflowText";
const meta: Meta<typeof OverflowText> = {
title: "Atoms/OverflowText",
component: OverflowText,
tags: ["autodocs"],
parameters: {
layout: "centered",
docs: {
description: {
component:
"Text component that automatically truncates overflowing content with ellipsis and shows a tooltip on hover when truncated. Supports both string and ReactNode values.",
},
},
},
argTypes: {
value: {
control: "text",
description: "The text content to display (string or ReactNode)",
},
className: {
control: "text",
description: "Additional CSS classes to customize styling",
},
},
args: {
value: "This is a sample text that may overflow",
className: "",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: function DefaultOverflowText(args) {
return (
<div className="w-64">
<OverflowText {...args} />
</div>
);
},
};
export const ShortText: Story = {
args: {
value: "Short text",
},
render: function ShortTextStory(args) {
return (
<div className="w-64">
<OverflowText {...args} />
</div>
);
},
};
export const LongText: Story = {
args: {
value:
"This is a very long text that will definitely overflow and show a tooltip when you hover over it",
},
render: function LongTextStory(args) {
return (
<div className="w-64">
<OverflowText {...args} />
</div>
);
},
};
export const CustomStyling: Story = {
args: {
value: "Text with custom styling",
className: "text-lg font-semibold text-indigo-600",
},
render: function CustomStylingStory(args) {
return (
<div className="w-64">
<OverflowText {...args} />
</div>
);
},
};
export const WithReactNode: Story = {
args: {
value: (
<span>
Text with <strong>bold</strong> and <em>italic</em> content
</span>
),
},
render: function WithReactNodeStory(args) {
return (
<div className="w-64">
<OverflowText {...args} />
</div>
);
},
};
export const DifferentWidths: Story = {
render: function DifferentWidthsStory() {
const longText =
"This text will truncate differently depending on the container width";
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<span className="text-xs text-zinc-500">Width: 200px</span>
<div className="w-[200px]">
<OverflowText value={longText} variant="body" />
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-zinc-500">Width: 300px</span>
<div className="w-[300px]">
<OverflowText value={longText} variant="body" />
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-zinc-500">Width: 400px</span>
<div className="w-[400px]">
<OverflowText value={longText} variant="body" />
</div>
</div>
</div>
);
},
};
export const FilePathExample: Story = {
args: {
value: "/very/long/path/to/a/file/that/might/overflow/in/the/ui.tsx",
},
render: function FilePathExampleStory(args) {
return (
<div className="w-64">
<OverflowText {...args} className="font-mono text-sm" />
</div>
);
},
};
export const URLExample: Story = {
args: {
value: "https://example.com/very/long/url/path/that/might/overflow",
},
render: function URLExampleStory(args) {
return (
<div className="w-64">
<OverflowText {...args} className="text-blue-600" />
</div>
);
},
};

View File

@@ -0,0 +1,100 @@
import { Text, type TextProps } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
interface Props extends Omit<TextProps, "children"> {
value: string | ReactNode;
}
export function OverflowText(props: Props) {
const elementRef = useRef<HTMLSpanElement | null>(null);
const [isTruncated, setIsTruncated] = useState(false);
function updateTruncation() {
const element = elementRef.current;
if (!element) {
return;
}
const hasOverflow = element.scrollWidth > element.clientWidth;
setIsTruncated(hasOverflow);
}
function setupResizeListener() {
function handleResize() {
updateTruncation();
}
window.addEventListener("resize", handleResize);
return function cleanupResizeListener() {
window.removeEventListener("resize", handleResize);
};
}
function setupObserver() {
const element = elementRef.current;
if (!element || typeof ResizeObserver === "undefined") {
return undefined;
}
function handleResizeObserver() {
updateTruncation();
}
const observer = new ResizeObserver(handleResizeObserver);
observer.observe(element);
return function disconnectObserver() {
observer.disconnect();
};
}
useEffect(() => {
if (typeof props.value === "string") updateTruncation();
}, [props.value]);
useEffect(setupResizeListener, []);
useEffect(setupObserver, []);
const { value, className, variant = "body", ...restProps } = props;
const content = (
<span
ref={elementRef}
className={cn(
"block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
)}
>
<Text variant={variant} className={className} {...restProps}>
{value}
</Text>
</span>
);
if (isTruncated) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>
{typeof value === "string" ? <p>{value}</p> : value}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return content;
}

View File

@@ -5,7 +5,7 @@ import { Cross2Icon } from "@radix-ui/react-icons";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { GoogleDrivePicker } from "./GoogleDrivePicker"; import { GoogleDrivePicker } from "./GoogleDrivePicker";
export interface GoogleDrivePickerInputProps { export interface Props {
config: GoogleDrivePickerConfig; config: GoogleDrivePickerConfig;
value: any; value: any;
onChange: (value: any) => void; onChange: (value: any) => void;
@@ -21,7 +21,7 @@ export function GoogleDrivePickerInput({
error, error,
className, className,
showRemoveButton = true, showRemoveButton = true,
}: GoogleDrivePickerInputProps) { }: Props) {
const [pickerError, setPickerError] = React.useState<string | null>(null); const [pickerError, setPickerError] = React.useState<string | null>(null);
const isMultiSelect = config.multiselect || false; const isMultiSelect = config.multiselect || false;
const hasAutoCredentials = !!config.auto_credentials; const hasAutoCredentials = !!config.auto_credentials;

View File

@@ -18,6 +18,8 @@ export function PublishAgentModal({
trigger, trigger,
targetState, targetState,
onStateChange, onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
}: Props) { }: Props) {
const { const {
// State // State
@@ -34,7 +36,12 @@ export function PublishAgentModal({
handleGoToBuilder, handleGoToBuilder,
handleSuccessFromInfo, handleSuccessFromInfo,
handleBack, handleBack,
} = usePublishAgentModal({ targetState, onStateChange }); } = usePublishAgentModal({
targetState,
onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
});
const { user, isUserLoading } = useSupabase(); const { user, isUserLoading } = useSupabase();
@@ -65,6 +72,7 @@ export function PublishAgentModal({
selectedAgentId={selectedAgentId} selectedAgentId={selectedAgentId}
selectedAgentVersion={selectedAgentVersion} selectedAgentVersion={selectedAgentVersion}
initialData={initialData} initialData={initialData}
isMarketplaceUpdate={!!currentState.submissionData}
/> />
); );
case "review": case "review":

View File

@@ -19,6 +19,7 @@ export function AgentInfoStep({
selectedAgentId, selectedAgentId,
selectedAgentVersion, selectedAgentVersion,
initialData, initialData,
isMarketplaceUpdate,
}: Props) { }: Props) {
const { const {
form, form,
@@ -34,6 +35,7 @@ export function AgentInfoStep({
selectedAgentId, selectedAgentId,
selectedAgentVersion, selectedAgentVersion,
initialData, initialData,
isMarketplaceUpdate,
}); });
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] = const [cronScheduleDialogOpen, setCronScheduleDialogOpen] =
@@ -65,6 +67,41 @@ export function AgentInfoStep({
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit} className="flex-grow overflow-y-auto p-6"> <form onSubmit={handleSubmit} className="flex-grow overflow-y-auto p-6">
{/* Changes summary field - only shown for updates */}
{isMarketplaceUpdate && (
<FormField
control={form.control}
name="changesSummary"
render={({ field }) => (
<div className="mb-6">
<Input
id={field.name}
label="What changed?"
type="textarea"
placeholder="Describe what's new or improved in this version..."
error={form.formState.errors.changesSummary?.message}
required
{...field}
/>
<Text variant="small" className="mt-1 text-gray-600">
This is required to help users understand what&apos;s
different in this update.
</Text>
</div>
)}
/>
)}
{/* Optional section label for updates */}
{isMarketplaceUpdate && (
<div className="mb-4">
<Text variant="body" className="font-medium text-gray-700">
Optional: Update any of the following details (or leave them
as-is)
</Text>
</div>
)}
<FormField <FormField
control={form.control} control={form.control}
name="title" name="title"

View File

@@ -25,6 +25,17 @@ export function useThumbnailImages({
const thumbnailsContainerRef = useRef<HTMLDivElement | null>(null); const thumbnailsContainerRef = useRef<HTMLDivElement | null>(null);
const { toast } = useToast(); const { toast } = useToast();
// Memoize the stringified version to detect actual changes
const initialImagesKey = JSON.stringify(initialImages);
// Update images when initialImages prop changes (by value, not reference)
useEffect(() => {
if (initialImages.length > 0) {
setImages(initialImages);
setSelectedImage(initialSelectedImage || initialImages[0]);
}
}, [initialImagesKey, initialSelectedImage]); // Use stringified key instead of array reference
// Notify parent when images change // Notify parent when images change
useEffect(() => { useEffect(() => {
onImagesChange(images); onImagesChange(images);

View File

@@ -1,45 +1,113 @@
import z from "zod"; import z from "zod";
import { validateYouTubeUrl } from "@/lib/utils"; import { validateYouTubeUrl } from "@/lib/utils";
export const publishAgentSchema = z.object({ // Create conditional schema that changes based on whether it's a marketplace update
title: z export const publishAgentSchemaFactory = (
.string() isMarketplaceUpdate: boolean = false,
.min(1, "Title is required") ) => {
.max(100, "Title must be less than 100 characters"), const baseSchema = {
subheader: z changesSummary: isMarketplaceUpdate
.string() ? z
.min(1, "Subheader is required") .string()
.max(200, "Subheader must be less than 200 characters"), .min(1, "Changes summary is required for updates")
slug: z .max(500, "Changes summary must be less than 500 characters")
.string() : z.string().optional(),
.min(1, "Slug is required") title: isMarketplaceUpdate
.max(50, "Slug must be less than 50 characters") ? z
.regex( .string()
/^[a-z0-9-]+$/, .optional()
"Slug can only contain lowercase letters, numbers, and hyphens", .refine(
), (val) => !val || val.length <= 100,
youtubeLink: z "Title must be less than 100 characters",
.string() )
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), : z
category: z.string().min(1, "Category is required"), .string()
description: z .min(1, "Title is required")
.string() .max(100, "Title must be less than 100 characters"),
.min(1, "Description is required") subheader: isMarketplaceUpdate
.max(1000, "Description must be less than 1000 characters"), ? z
recommendedScheduleCron: z.string().optional(), .string()
instructions: z .optional()
.string() .refine(
.optional() (val) => !val || val.length <= 200,
.refine( "Subheader must be less than 200 characters",
(val) => !val || val.length <= 2000, )
"Instructions must be less than 2000 characters", : z
), .string()
agentOutputDemo: z .min(1, "Subheader is required")
.string() .max(200, "Subheader must be less than 200 characters"),
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"), slug: isMarketplaceUpdate
}); ? z
.string()
.optional()
.refine(
(val) => !val || (val.length <= 50 && /^[a-z0-9-]+$/.test(val)),
"Slug can only contain lowercase letters, numbers, and hyphens",
)
: z
.string()
.min(1, "Slug is required")
.max(50, "Slug must be less than 50 characters")
.regex(
/^[a-z0-9-]+$/,
"Slug can only contain lowercase letters, numbers, and hyphens",
),
youtubeLink: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || validateYouTubeUrl(val),
"Please enter a valid YouTube URL",
)
: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
category: isMarketplaceUpdate
? z.string().optional()
: z.string().min(1, "Category is required"),
description: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || val.length <= 1000,
"Description must be less than 1000 characters",
)
: z
.string()
.min(1, "Description is required")
.max(1000, "Description must be less than 1000 characters"),
recommendedScheduleCron: z.string().optional(),
instructions: z
.string()
.optional()
.refine(
(val) => !val || val.length <= 2000,
"Instructions must be less than 2000 characters",
),
agentOutputDemo: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || validateYouTubeUrl(val),
"Please enter a valid YouTube URL",
)
: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
};
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>; return z.object(baseSchema);
};
// Default schema for backwards compatibility
export const publishAgentSchema = publishAgentSchemaFactory(false);
export type PublishAgentFormData = z.infer<
ReturnType<typeof publishAgentSchemaFactory>
>;
export interface PublishAgentInfoInitialData { export interface PublishAgentInfoInitialData {
agent_id: string; agent_id: string;
@@ -54,4 +122,5 @@ export interface PublishAgentInfoInitialData {
recommendedScheduleCron?: string; recommendedScheduleCron?: string;
instructions?: string; instructions?: string;
agentOutputDemo?: string; agentOutputDemo?: string;
changesSummary?: string;
} }

View File

@@ -9,7 +9,7 @@ import * as Sentry from "@sentry/nextjs";
import { import {
PublishAgentFormData, PublishAgentFormData,
PublishAgentInfoInitialData, PublishAgentInfoInitialData,
publishAgentSchema, publishAgentSchemaFactory,
} from "./helpers"; } from "./helpers";
export interface Props { export interface Props {
@@ -18,6 +18,7 @@ export interface Props {
selectedAgentId: string | null; selectedAgentId: string | null;
selectedAgentVersion: number | null; selectedAgentVersion: number | null;
initialData?: PublishAgentInfoInitialData; initialData?: PublishAgentInfoInitialData;
isMarketplaceUpdate?: boolean;
} }
export function useAgentInfoStep({ export function useAgentInfoStep({
@@ -26,6 +27,7 @@ export function useAgentInfoStep({
selectedAgentId, selectedAgentId,
selectedAgentVersion, selectedAgentVersion,
initialData, initialData,
isMarketplaceUpdate = false,
}: Props) { }: Props) {
const [agentId, setAgentId] = useState<string | null>(null); const [agentId, setAgentId] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]); const [images, setImages] = useState<string[]>([]);
@@ -36,8 +38,9 @@ export function useAgentInfoStep({
const api = useBackendAPI(); const api = useBackendAPI();
const form = useForm<PublishAgentFormData>({ const form = useForm<PublishAgentFormData>({
resolver: zodResolver(publishAgentSchema), resolver: zodResolver(publishAgentSchemaFactory(isMarketplaceUpdate)),
defaultValues: { defaultValues: {
changesSummary: "",
title: "", title: "",
subheader: "", subheader: "",
slug: "", slug: "",
@@ -61,6 +64,7 @@ export function useAgentInfoStep({
// Update form with initial data // Update form with initial data
form.reset({ form.reset({
changesSummary: initialData.changesSummary || "",
title: initialData.title, title: initialData.title,
subheader: initialData.subheader, subheader: initialData.subheader,
slug: initialData.slug.toLocaleLowerCase().trim(), slug: initialData.slug.toLocaleLowerCase().trim(),
@@ -104,9 +108,10 @@ export function useAgentInfoStep({
agent_output_demo_url: data.agentOutputDemo || "", agent_output_demo_url: data.agentOutputDemo || "",
agent_id: selectedAgentId || "", agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0, agent_version: selectedAgentVersion || 0,
slug: data.slug.replace(/\s+/g, "-"), slug: (data.slug || "").replace(/\s+/g, "-"),
categories: filteredCategories, categories: filteredCategories,
recommended_schedule_cron: data.recommendedScheduleCron || null, recommended_schedule_cron: data.recommendedScheduleCron || null,
changes_summary: data.changesSummary || null,
} as any); } as any);
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({

View File

@@ -52,7 +52,7 @@ export function AgentReviewStep({
</Text> </Text>
<Text <Text
variant="large" variant="large"
className="line-clamp-1 text-ellipsis text-center !text-neutral-500" className="line-clamp-1 text-ellipsis text-center text-neutral-500"
> >
{subheader} {subheader}
</Text> </Text>
@@ -80,7 +80,7 @@ export function AgentReviewStep({
{description ? ( {description ? (
<Text <Text
variant="large" variant="large"
className="line-clamp-1 text-ellipsis pt-2 text-center !text-neutral-500" className="line-clamp-1 text-ellipsis pt-2 text-center text-neutral-500"
> >
{description} {description}
</Text> </Text>

View File

@@ -9,6 +9,7 @@ import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { useAgentSelectStep } from "./useAgentSelectStep"; import { useAgentSelectStep } from "./useAgentSelectStep";
import { scrollbarStyles } from "@/components/styles/scrollbars"; import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
interface Props { interface Props {
onSelect: (agentId: string, agentVersion: number) => void; onSelect: (agentId: string, agentVersion: number) => void;
@@ -22,6 +23,7 @@ interface Props {
imageSrc: string; imageSrc: string;
recommendedScheduleCron: string | null; recommendedScheduleCron: string | null;
}, },
publishedSubmissionData?: StoreSubmission | null,
) => void; ) => void;
onOpenBuilder: () => void; onOpenBuilder: () => void;
} }
@@ -42,6 +44,8 @@ export function AgentSelectStep({
// Handlers // Handlers
handleAgentClick, handleAgentClick,
handleNext, handleNext,
// Utils
getPublishedVersion,
// Computed // Computed
isNextDisabled, isNextDisabled,
} = useAgentSelectStep({ onSelect, onNext }); } = useAgentSelectStep({ onSelect, onNext });
@@ -131,26 +135,17 @@ export function AgentSelectStep({
<div className="p-2"> <div className="p-2">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{agents.map((agent) => ( {agents.map((agent) => (
<div <button
key={agent.id} key={agent.id}
data-testid="agent-card" data-testid="agent-card"
className={`cursor-pointer select-none overflow-hidden rounded-2xl border border-neutral-200 shadow-sm transition-all ${ onClick={() =>
handleAgentClick(agent.name, agent.id, agent.version)
}
className={`w-full select-none overflow-hidden rounded-2xl border border-neutral-200 text-left shadow-sm transition-all ${
selectedAgentId === agent.id selectedAgentId === agent.id
? "border-transparent shadow-none ring-4 ring-violet-600" ? "border-transparent shadow-none ring-4 ring-violet-600"
: "hover:shadow-md" : "hover:shadow-md"
}`} }`}
onClick={() =>
handleAgentClick(agent.name, agent.id, agent.version)
}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleAgentClick(agent.name, agent.id, agent.version);
}
}}
tabIndex={0}
role="button"
aria-pressed={selectedAgentId === agent.id}
> >
<div className="relative h-32 bg-zinc-400 sm:h-40"> <div className="relative h-32 bg-zinc-400 sm:h-40">
<Image <Image
@@ -162,12 +157,44 @@ export function AgentSelectStep({
/> />
</div> </div>
<div className="flex flex-col gap-2 p-3"> <div className="flex flex-col gap-2 p-3">
<Text variant="large-medium">{agent.name}</Text> <Text variant="large-medium" className="line-clamp-2">
<Text variant="small" className="!text-neutral-500"> {agent.name}
Edited {agent.lastEdited}
</Text> </Text>
<div className="flex items-center justify-between gap-2">
<div className="flex-1">
<Text variant="small" className="text-neutral-500">
Edited {agent.lastEdited}
</Text>
{agent.isMarketplaceUpdate &&
(() => {
const publishedVersion = getPublishedVersion(
agent.id,
);
return (
publishedVersion && (
<Text
variant="small"
className="block text-neutral-500"
>
v{publishedVersion} v{agent.version}
</Text>
)
);
})()}
</div>
{agent.isMarketplaceUpdate && (
<span className="shrink-0 rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">
Update
</span>
)}
{!agent.isMarketplaceUpdate && (
<span className="shrink-0 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200">
New
</span>
)}
</div>
</div> </div>
</div> </button>
))} ))}
</div> </div>
</div> </div>

View File

@@ -1,5 +1,11 @@
import * as React from "react"; import * as React from "react";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store"; import {
useGetV2GetMyAgents,
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
export interface Agent { export interface Agent {
name: string; name: string;
@@ -9,6 +15,7 @@ export interface Agent {
imageSrc: string; imageSrc: string;
description: string; description: string;
recommendedScheduleCron: string | null; recommendedScheduleCron: string | null;
isMarketplaceUpdate: boolean; // true if this is an update to existing published agent
} }
interface UseAgentSelectStepProps { interface UseAgentSelectStepProps {
@@ -22,6 +29,7 @@ interface UseAgentSelectStepProps {
imageSrc: string; imageSrc: string;
recommendedScheduleCron: string | null; recommendedScheduleCron: string | null;
}, },
publishedSubmissionData?: StoreSubmission | null, // For pre-filling updates
) => void; ) => void;
} }
@@ -36,27 +44,88 @@ export function useAgentSelectStep({
number | null number | null
>(null); >(null);
const { data: myAgents, isLoading, error } = useGetV2GetMyAgents(); const {
data: myAgents,
isLoading: agentsLoading,
error: agentsError,
} = useGetV2GetMyAgents();
const {
data: mySubmissions,
isLoading: submissionsLoading,
error: submissionsError,
} = useGetV2ListMySubmissions();
const agents: Agent[] = const isLoading = agentsLoading || submissionsLoading;
(myAgents?.status === 200 && const error = agentsError || submissionsError;
myAgents.data.agents
.map( const agents: Agent[] = React.useMemo(() => {
(agent): Agent => ({ // Properly handle API responses with okData helper
name: agent.agent_name, const agentsData = (okData(myAgents) as any)?.agents || [];
id: agent.agent_id, const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
version: agent.agent_version,
lastEdited: agent.last_edited.toLocaleDateString(), if (agentsData.length === 0) {
imageSrc: agent.agent_image || "https://picsum.photos/300/200", return [];
description: agent.description || "", }
recommendedScheduleCron: agent.recommended_schedule_cron ?? null,
}), return agentsData
) .map((agent: MyAgent): Agent | null => {
.sort( // Find the highest published agent_version for this agent from approved submissions
(a: Agent, b: Agent) => const publishedVersion = submissionsData
new Date(b.lastEdited).getTime() - new Date(a.lastEdited).getTime(), .filter(
)) || (s: StoreSubmission) =>
[]; s.status === "APPROVED" && s.agent_id === agent.agent_id,
)
.reduce(
(max: number | undefined, s: StoreSubmission) =>
max === undefined || s.agent_version > max
? s.agent_version
: max,
undefined,
);
const isMarketplaceUpdate =
publishedVersion !== undefined &&
agent.agent_version > publishedVersion;
const isNewAgent = publishedVersion === undefined;
// Only include agents that are either new or have newer versions than published
if (!isNewAgent && !isMarketplaceUpdate) {
return null;
}
return {
name: agent.agent_name,
id: agent.agent_id,
version: agent.agent_version,
lastEdited: agent.last_edited.toLocaleDateString(),
imageSrc: agent.agent_image || "https://picsum.photos/300/200",
description: agent.description || "",
recommendedScheduleCron: agent.recommended_schedule_cron ?? null,
isMarketplaceUpdate,
};
})
.filter((agent: Agent | null): agent is Agent => agent !== null)
.sort(
(a: Agent, b: Agent) =>
new Date(b.lastEdited).getTime() - new Date(a.lastEdited).getTime(),
);
}, [myAgents, mySubmissions]);
// Function to get published submission data for pre-filling updates
const getPublishedSubmissionData = (agentId: string) => {
const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
const approvedSubmissions = submissionsData
.filter(
(submission: StoreSubmission) =>
submission.agent_id === agentId && submission.status === "APPROVED",
)
.sort(
(a: StoreSubmission, b: StoreSubmission) =>
b.agent_version - a.agent_version,
);
return approvedSubmissions[0] || null;
};
const handleAgentClick = ( const handleAgentClick = (
_: string, _: string,
@@ -74,16 +143,42 @@ export function useAgentSelectStep({
(agent) => agent.id === selectedAgentId, (agent) => agent.id === selectedAgentId,
); );
if (selectedAgent) { if (selectedAgent) {
onNext(selectedAgentId, selectedAgentVersion, { // Get published submission data for pre-filling if this is an update
name: selectedAgent.name, const publishedSubmissionData = selectedAgent.isMarketplaceUpdate
description: selectedAgent.description, ? getPublishedSubmissionData(selectedAgentId)
imageSrc: selectedAgent.imageSrc, : undefined;
recommendedScheduleCron: selectedAgent.recommendedScheduleCron,
}); onNext(
selectedAgentId,
selectedAgentVersion,
{
name: selectedAgent.name,
description: selectedAgent.description,
imageSrc: selectedAgent.imageSrc,
recommendedScheduleCron: selectedAgent.recommendedScheduleCron,
},
publishedSubmissionData,
);
} }
} }
}; };
// Helper to get published version for an agent
const getPublishedVersion = (agentId: string): number | undefined => {
const submissionsData = (okData(mySubmissions) as any)?.submissions || [];
return submissionsData
.filter(
(s: StoreSubmission) =>
s.status === "APPROVED" && s.agent_id === agentId,
)
.reduce(
(max: number | undefined, s: StoreSubmission) =>
max === undefined || s.agent_version > max ? s.agent_version : max,
undefined,
);
};
return { return {
// Data // Data
agents, agents,
@@ -94,6 +189,9 @@ export function useAgentSelectStep({
// Handlers // Handlers
handleAgentClick, handleAgentClick,
handleNext, handleNext,
// Utils
getPublishedSubmissionData,
getPublishedVersion,
// Computed // Computed
isNextDisabled: !selectedAgentId || !selectedAgentVersion, isNextDisabled: !selectedAgentId || !selectedAgentVersion,
}; };

View File

@@ -8,4 +8,8 @@ export const emptyModalState = {
category: "", category: "",
description: "", description: "",
recommendedScheduleCron: "", recommendedScheduleCron: "",
instructions: "",
agentOutputDemo: "",
changesSummary: "",
additionalImages: [],
}; };

View File

@@ -3,6 +3,12 @@ import { useCallback, useEffect, useState } from "react";
import { PublishAgentInfoInitialData } from "./components/AgentInfoStep/helpers"; import { PublishAgentInfoInitialData } from "./components/AgentInfoStep/helpers";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { emptyModalState } from "./helpers"; import { emptyModalState } from "./helpers";
import {
useGetV2GetMyAgents,
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
const defaultTargetState: PublishState = { const defaultTargetState: PublishState = {
isOpen: false, isOpen: false,
@@ -22,9 +28,16 @@ export interface Props {
trigger?: React.ReactNode; trigger?: React.ReactNode;
targetState?: PublishState; targetState?: PublishState;
onStateChange?: (state: PublishState) => void; onStateChange?: (state: PublishState) => void;
preSelectedAgentId?: string;
preSelectedAgentVersion?: number;
} }
export function usePublishAgentModal({ targetState, onStateChange }: Props) { export function usePublishAgentModal({
targetState,
onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
}: Props) {
const [currentState, setCurrentState] = useState<PublishState>( const [currentState, setCurrentState] = useState<PublishState>(
targetState || defaultTargetState, targetState || defaultTargetState,
); );
@@ -42,14 +55,20 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
const [_, setSelectedAgent] = useState<string | null>(null); const [_, setSelectedAgent] = useState<string | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null); const [selectedAgentId, setSelectedAgentId] = useState<string | null>(
preSelectedAgentId || null,
);
const [selectedAgentVersion, setSelectedAgentVersion] = useState< const [selectedAgentVersion, setSelectedAgentVersion] = useState<
number | null number | null
>(null); >(preSelectedAgentVersion || null);
const router = useRouter(); const router = useRouter();
// Fetch agent data for pre-populating form when agent is pre-selected
const { data: myAgents } = useGetV2GetMyAgents();
const { data: mySubmissions } = useGetV2ListMySubmissions();
// Sync currentState with targetState when it changes from outside // Sync currentState with targetState when it changes from outside
useEffect(() => { useEffect(() => {
if (targetState) { if (targetState) {
@@ -60,13 +79,90 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
// Reset internal state when modal opens // Reset internal state when modal opens
useEffect(() => { useEffect(() => {
if (!targetState) return; if (!targetState) return;
if (targetState.isOpen && targetState.step === "select") { if (targetState.isOpen) {
setSelectedAgent(null); setSelectedAgent(null);
setSelectedAgentId(null); setSelectedAgentId(preSelectedAgentId || null);
setSelectedAgentVersion(null); setSelectedAgentVersion(preSelectedAgentVersion || null);
setInitialData(emptyModalState); setInitialData(emptyModalState);
} }
}, [targetState]); }, [targetState, preSelectedAgentId, preSelectedAgentVersion]);
// Pre-populate form data when modal opens with info step and pre-selected agent
useEffect(() => {
if (
!targetState?.isOpen ||
targetState.step !== "info" ||
!preSelectedAgentId ||
!preSelectedAgentVersion
)
return;
const agentsData = okData(myAgents) as any;
const submissionsData = okData(mySubmissions) as any;
if (!agentsData || !submissionsData) return;
// Find the agent data
const agent = agentsData.agents?.find(
(a: MyAgent) => a.agent_id === preSelectedAgentId,
);
if (!agent) return;
// Find published submission data for this agent (for updates)
const publishedSubmissionData = submissionsData.submissions
?.filter(
(s: StoreSubmission) =>
s.status === "APPROVED" && s.agent_id === preSelectedAgentId,
)
.sort(
(a: StoreSubmission, b: StoreSubmission) =>
b.agent_version - a.agent_version,
)[0];
// Populate initial data (same logic as handleNextFromSelect)
const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
? {
agent_id: preSelectedAgentId,
title: publishedSubmissionData.name,
subheader: publishedSubmissionData.sub_heading || "",
description: publishedSubmissionData.description,
instructions: publishedSubmissionData.instructions || "",
youtubeLink: publishedSubmissionData.video_url || "",
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
additionalImages: [
...new Set(publishedSubmissionData.image_urls || []),
].filter(Boolean) as string[],
category: publishedSubmissionData.categories?.[0] || "",
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
slug: publishedSubmissionData.slug,
recommendedScheduleCron: agent.recommended_schedule_cron || "",
changesSummary: "", // Empty for user to fill in what changed
}
: {
...emptyModalState,
agent_id: preSelectedAgentId,
title: agent.agent_name,
description: agent.description || "",
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
slug: agent.agent_name.replace(/ /g, "-"),
recommendedScheduleCron: agent.recommended_schedule_cron || "",
};
setInitialData(initialFormData);
// Update the state with the submission data if this is an update
if (publishedSubmissionData) {
setCurrentState((prevState) => ({
...prevState,
submissionData: publishedSubmissionData,
}));
}
}, [
targetState,
preSelectedAgentId,
preSelectedAgentVersion,
myAgents,
mySubmissions,
]);
function handleClose() { function handleClose() {
// Reset all internal state // Reset all internal state
@@ -97,20 +193,43 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
imageSrc: string; imageSrc: string;
recommendedScheduleCron: string | null; recommendedScheduleCron: string | null;
}, },
publishedSubmissionData?: StoreSubmission | null,
) { ) {
setInitialData({ // Pre-populate with published data if this is an update, otherwise use agent data
...emptyModalState, const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
agent_id: agentId, ? {
title: agentData.name, agent_id: agentId,
description: agentData.description, title: publishedSubmissionData.name,
thumbnailSrc: agentData.imageSrc, subheader: publishedSubmissionData.sub_heading || "",
slug: agentData.name.replace(/ /g, "-"), description: publishedSubmissionData.description,
recommendedScheduleCron: agentData.recommendedScheduleCron || "", instructions: publishedSubmissionData.instructions || "",
}); youtubeLink: publishedSubmissionData.video_url || "",
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
additionalImages: [
...new Set(publishedSubmissionData.image_urls || []),
].filter(Boolean) as string[],
category: publishedSubmissionData.categories?.[0] || "", // Take first category
thumbnailSrc: agentData.imageSrc, // Use current agent image
slug: publishedSubmissionData.slug,
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
changesSummary: "", // Empty for user to fill in what changed
}
: {
...emptyModalState,
agent_id: agentId,
title: agentData.name,
description: agentData.description,
thumbnailSrc: agentData.imageSrc,
slug: agentData.name.replace(/ /g, "-"),
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
};
setInitialData(initialFormData);
updateState({ updateState({
...currentState, ...currentState,
step: "info", step: "info",
submissionData: publishedSubmissionData || null,
}); });
setSelectedAgentId(agentId); setSelectedAgentId(agentId);

View File

@@ -0,0 +1,58 @@
/**
* Marketplace-specific helper functions that can be reused across different marketplace screens
*/
/**
* Calculate the latest marketplace version from agent graph versions
*/
export function getLatestMarketplaceVersion(
agentGraphVersions?: string[],
): number | undefined {
if (!agentGraphVersions?.length) return undefined;
return Math.max(...agentGraphVersions.map((v: string) => parseInt(v, 10)));
}
/**
* Check if the current user is the creator of the agent
*/
export function isUserCreator(
creator: string,
currentUser: { email?: string } | null,
): boolean {
if (!currentUser?.email) return false;
const userHandle = currentUser.email.split("@")[0]?.toLowerCase() || "";
return creator.toLowerCase().includes(userHandle);
}
/**
* Calculate update status for an agent
*/
export function calculateUpdateStatus({
latestMarketplaceVersion,
currentVersion,
isUserCreator,
isAgentAddedToLibrary,
}: {
latestMarketplaceVersion?: number;
currentVersion: number;
isUserCreator: boolean;
isAgentAddedToLibrary: boolean;
}) {
if (!latestMarketplaceVersion) {
return { hasUpdate: false, hasUnpublishedChanges: false };
}
const hasUnpublishedChanges =
isUserCreator &&
isAgentAddedToLibrary &&
currentVersion > latestMarketplaceVersion;
const hasUpdate =
isAgentAddedToLibrary &&
!isUserCreator &&
latestMarketplaceVersion > currentVersion;
return { hasUpdate, hasUnpublishedChanges };
}

View File

@@ -19,7 +19,7 @@ export function MobileNavbarMenuItem({
onClick, onClick,
}: Props) { }: Props) {
const content = ( const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0]"> <div className="inline-flex w-full items-center justify-start gap-4 py-2 hover:rounded hover:bg-[#e0e0e0]">
{getAccountMenuOptionIcon(icon)} {getAccountMenuOptionIcon(icon)}
<div className="relative"> <div className="relative">
<div <div

View File

@@ -5,6 +5,7 @@ import * as React from "react";
interface BreadcrumbItem { interface BreadcrumbItem {
name: string; name: string;
link: string; link: string;
testId?: string;
} }
interface Props { interface Props {
@@ -19,6 +20,7 @@ export function Breadcrumbs({ items }: Props) {
<Link <Link
href={item.link} href={item.link}
className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline" className="text-[0.75rem] font-[400] text-zinc-600 transition-colors hover:text-zinc-900 hover:no-underline"
data-testid={item.testId}
> >
{item.name} {item.name}
</Link> </Link>

View File

@@ -3,8 +3,7 @@ const commonStyles = {
title: "font-poppins text-md md:text-lg leading-none", title: "font-poppins text-md md:text-lg leading-none",
overlay: overlay:
"fixed inset-0 z-50 bg-stone-500/20 dark:bg-black/50 backdrop-blur-md animate-fade-in", "fixed inset-0 z-50 bg-stone-500/20 dark:bg-black/50 backdrop-blur-md animate-fade-in",
content: content: "bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full",
"overflow-y-hidden bg-white p-6 fixed rounded-2xlarge flex flex-col z-50 w-full",
}; };
// Modal specific styles // Modal specific styles

View File

@@ -9,16 +9,20 @@ import ReactMarkdown from "react-markdown";
type Props = { type Props = {
description?: string; description?: string;
iconSize?: number;
}; };
export function InformationTooltip({ description }: Props) { export function InformationTooltip({ description, iconSize = 24 }: Props) {
if (!description) return null; if (!description) return null;
return ( return (
<TooltipProvider delayDuration={400}> <TooltipProvider delayDuration={400}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Info className="rounded-full p-1 hover:bg-slate-50" size={24} /> <Info
className="rounded-full p-1 hover:bg-slate-50"
size={iconSize}
/>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<ReactMarkdown <ReactMarkdown

View File

@@ -0,0 +1,437 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import {
ScrollableTabs,
ScrollableTabsContent,
ScrollableTabsList,
ScrollableTabsTrigger,
} from "./ScrollableTabs";
const meta = {
title: "Molecules/ScrollableTabs",
component: ScrollableTabs,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof ScrollableTabs>;
export default meta;
type Story = StoryObj<typeof meta>;
function ScrollableTabsDemo() {
return (
<div className="flex flex-col gap-8 p-8">
<h2 className="text-2xl font-bold">ScrollableTabs Examples</h2>
<div className="space-y-6">
<div>
<h3 className="mb-4 text-lg font-semibold">
Short Content (Tabs Hidden)
</h3>
<div className="h-[300px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">
Password
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">
Settings
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-4 text-sm">
Make changes to your account here. Click save when you&apos;re
done.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-4 text-sm">
Change your password here. After saving, you&apos;ll be logged
out.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-4 text-sm">
Update your preferences and settings here.
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">
Long Content (Tabs Visible)
</h3>
<div className="h-[400px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">
Password
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">
Settings
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Account Settings
</h4>
<p className="mb-4">
Make changes to your account here. Click save when
you&apos;re done.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident.
</p>
<p>
Sed ut perspiciatis unde omnis iste natus error sit
voluptatem accusantium doloremque laudantium, totam rem
aperiam.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Password Settings
</h4>
<p className="mb-4">
Change your password here. After saving, you&apos;ll be
logged out.
</p>
<p className="mb-4">
At vero eos et accusamus et iusto odio dignissimos ducimus
qui blanditiis praesentium voluptatum deleniti atque
corrupti quos dolores et quas molestias excepturi sint
occaecati cupiditate.
</p>
<p className="mb-4">
Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore, cum soluta nobis est eligendi optio
cumque nihil impedit quo minus.
</p>
<p>
Temporibus autem quibusdam et aut officiis debitis aut rerum
necessitatibus saepe eveniet ut et voluptates repudiandae
sint.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
General Settings
</h4>
<p className="mb-4">
Update your preferences and settings here.
</p>
<p className="mb-4">
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit, sed quia consequuntur magni dolores eos qui
ratione voluptatem sequi nesciunt.
</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit
amet, consectetur, adipisci velit, sed quia non numquam eius
modi tempora incidunt ut labore et dolore magnam aliquam
quaerat voluptatem.
</p>
<p>
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Many Tabs</h3>
<div className="h-[500px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="overview" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="overview">
Overview
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="analytics">
Analytics
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="reports">
Reports
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="notifications">
Notifications
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="integrations">
Integrations
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="billing">
Billing
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="overview">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">
Dashboard Overview
</h4>
<p className="mb-4">
Dashboard overview with key metrics and recent activity.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna
aliqua.
</p>
<p>
Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="analytics">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Analytics</h4>
<p className="mb-4">
Detailed analytics and performance metrics.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur.
</p>
<p>
Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="reports">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Reports</h4>
<p className="mb-4">
Generate and view reports for your account.
</p>
<p className="mb-4">
Sed ut perspiciatis unde omnis iste natus error sit
voluptatem accusantium doloremque laudantium.
</p>
<p>
Totam rem aperiam, eaque ipsa quae ab illo inventore
veritatis et quasi architecto beatae vitae dicta sunt
explicabo.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="notifications">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Notifications</h4>
<p className="mb-4">Manage your notification preferences.</p>
<p className="mb-4">
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
odit aut fugit.
</p>
<p>
Sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="integrations">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Integrations</h4>
<p className="mb-4">
Connect and manage third-party integrations.
</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit
amet.
</p>
<p>
Consectetur, adipisci velit, sed quia non numquam eius modi
tempora incidunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="billing">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Billing</h4>
<p className="mb-4">
View and manage your billing information.
</p>
<p className="mb-4">
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam.
</p>
<p>
Nisi ut aliquid ex ea commodi consequatur? Quis autem vel
eum iure reprehenderit qui in ea voluptate velit esse.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
</div>
</div>
);
}
export const Default = {
render: () => <ScrollableTabsDemo />,
} satisfies Story;
export const ShortContent = {
render: () => (
<div className="p-8">
<div className="h-[200px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="account" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="account">
Account
</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="password">
Password
</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="account">
<div className="p-4 text-sm">
Make changes to your account here. Click save when you&apos;re
done.
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="password">
<div className="p-4 text-sm">
Change your password here. After saving, you&apos;ll be logged
out.
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
),
} satisfies Story;
export const LongContent = {
render: () => (
<div className="p-8">
<div className="h-[600px] overflow-y-auto border border-zinc-200">
<ScrollableTabs defaultValue="tab1" className="h-full">
<ScrollableTabsList>
<ScrollableTabsTrigger value="tab1">Account</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab2">Password</ScrollableTabsTrigger>
<ScrollableTabsTrigger value="tab3">Settings</ScrollableTabsTrigger>
</ScrollableTabsList>
<ScrollableTabsContent value="tab1">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Account Settings</h4>
<p className="mb-4">
Make changes to your account here. Click save when you&apos;re
done.
</p>
<p className="mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</p>
<p className="mb-4">
Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
<p className="mb-4">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
quae ab illo inventore veritatis et quasi architecto beatae
vitae dicta sunt explicabo.
</p>
<p>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit
aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab2">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">Password Settings</h4>
<p className="mb-4">
Change your password here. After saving, you&apos;ll be logged
out.
</p>
<p className="mb-4">
At vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos
dolores et quas molestias excepturi sint occaecati cupiditate
non provident.
</p>
<p className="mb-4">
Similique sunt in culpa qui officia deserunt mollitia animi, id
est laborum et dolorum fuga. Et harum quidem rerum facilis est
et expedita distinctio.
</p>
<p className="mb-4">
Nam libero tempore, cum soluta nobis est eligendi optio cumque
nihil impedit quo minus id quod maxime placeat facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus.
</p>
<p>
Temporibus autem quibusdam et aut officiis debitis aut rerum
necessitatibus saepe eveniet ut et voluptates repudiandae sint
et molestiae non recusandae.
</p>
</div>
</ScrollableTabsContent>
<ScrollableTabsContent value="tab3">
<div className="p-8 text-sm">
<h4 className="mb-4 text-lg font-semibold">General Settings</h4>
<p className="mb-4">Update your preferences and settings here.</p>
<p className="mb-4">
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi
tempora incidunt ut labore et dolore magnam aliquam quaerat
voluptatem.
</p>
<p className="mb-4">
Ut enim ad minima veniam, quis nostrum exercitationem ullam
corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
consequatur? Quis autem vel eum iure reprehenderit qui in ea
voluptate velit esse quam nihil molestiae consequatur.
</p>
<p className="mb-4">
Vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At
vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos
dolores.
</p>
<p>
Et quas molestias excepturi sint occaecati cupiditate non
provident, similique sunt in culpa qui officia deserunt mollitia
animi, id est laborum et dolorum fuga.
</p>
</div>
</ScrollableTabsContent>
</ScrollableTabs>
</div>
</div>
),
} satisfies Story;

View File

@@ -0,0 +1,59 @@
"use client";
import { cn } from "@/lib/utils";
import { Children } from "react";
import { ScrollableTabsContent } from "./components/ScrollableTabsContent";
import { ScrollableTabsList } from "./components/ScrollableTabsList";
import { ScrollableTabsTrigger } from "./components/ScrollableTabsTrigger";
import { ScrollableTabsContext } from "./context";
import { findContentElements, findListElement } from "./helpers";
import { useScrollableTabsInternal } from "./useScrollableTabs";
interface Props {
children?: React.ReactNode;
className?: string;
defaultValue?: string;
}
export function ScrollableTabs({ children, className, defaultValue }: Props) {
const {
activeValue,
setActiveValue,
registerContent,
scrollToSection,
scrollContainer,
contentContainerRef,
} = useScrollableTabsInternal({ defaultValue });
const childrenArray = Children.toArray(children);
const listElement = findListElement(childrenArray);
const contentElements = findContentElements(childrenArray);
return (
<ScrollableTabsContext.Provider
value={{
activeValue,
setActiveValue,
registerContent,
scrollToSection,
scrollContainer,
}}
>
<div className={cn("relative flex flex-col", className)}>
{listElement}
<div
ref={(node) => {
if (contentContainerRef) {
contentContainerRef.current = node;
}
}}
className="max-h-[64rem] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700"
>
<div className="min-h-full pb-[200px]">{contentElements}</div>
</div>
</div>
</ScrollableTabsContext.Provider>
);
}
export { ScrollableTabsContent, ScrollableTabsList, ScrollableTabsTrigger };

View File

@@ -0,0 +1,48 @@
"use client";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useScrollableTabs } from "../context";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
value: string;
}
export const ScrollableTabsContent = React.forwardRef<HTMLDivElement, Props>(
function ScrollableTabsContent(
{ className, value, children, ...props },
ref,
) {
const { registerContent } = useScrollableTabs();
const contentRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (contentRef.current) {
registerContent(value, contentRef.current);
}
return () => {
registerContent(value, null);
};
}, [value, registerContent]);
return (
<div
ref={(node) => {
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
contentRef.current = node;
}}
data-scrollable-tab-content
data-value={value}
className={cn("focus-visible:outline-none", className)}
{...props}
>
{children}
</div>
);
},
);
ScrollableTabsContent.displayName = "ScrollableTabsContent";

View File

@@ -0,0 +1,52 @@
"use client";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useScrollableTabs } from "../context";
export const ScrollableTabsList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(function ScrollableTabsList({ className, children, ...props }, ref) {
const { activeValue } = useScrollableTabs();
const [activeTabElement, setActiveTabElement] =
React.useState<HTMLElement | null>(null);
React.useEffect(() => {
const activeButton = Array.from(
document.querySelectorAll<HTMLElement>(
'[data-scrollable-tab-trigger][data-value="' + activeValue + '"]',
),
)[0];
if (activeButton) {
setActiveTabElement(activeButton);
}
}, [activeValue]);
return (
<div className="relative" ref={ref}>
<div
className={cn(
"inline-flex w-full items-center justify-start border-b border-zinc-100",
className,
)}
{...props}
>
{children}
</div>
{activeTabElement && (
<div
className="transition-left transition-right absolute bottom-0 h-0.5 bg-purple-600 duration-200 ease-in-out"
style={{
left: activeTabElement.offsetLeft,
width: activeTabElement.offsetWidth,
willChange: "left, width",
}}
/>
)}
</div>
);
});
ScrollableTabsList.displayName = "ScrollableTabsList";

View File

@@ -0,0 +1,53 @@
"use client";
import { cn } from "@/lib/utils";
import * as React from "react";
import { useScrollableTabs } from "../context";
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
}
export const ScrollableTabsTrigger = React.forwardRef<HTMLButtonElement, Props>(
function ScrollableTabsTrigger(
{ className, value, children, ...props },
ref,
) {
const { activeValue, scrollToSection } = useScrollableTabs();
const elementRef = React.useRef<HTMLButtonElement>(null);
const isActive = activeValue === value;
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
scrollToSection(value);
props.onClick?.(e);
}
return (
<button
type="button"
ref={(node) => {
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
elementRef.current = node;
}}
data-scrollable-tab-trigger
data-value={value}
onClick={handleClick}
className={cn(
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-3 font-sans text-[0.875rem] font-medium leading-[1.5rem] text-zinc-700 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive && "text-purple-600",
className,
)}
{...props}
>
{children}
</button>
);
},
);
ScrollableTabsTrigger.displayName = "ScrollableTabsTrigger";

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { createContext, useContext } from "react";
interface ScrollableTabsContextValue {
activeValue: string | null;
setActiveValue: React.Dispatch<React.SetStateAction<string | null>>;
registerContent: (value: string, element: HTMLElement | null) => void;
scrollToSection: (value: string) => void;
scrollContainer: HTMLElement | null;
}
export const ScrollableTabsContext = createContext<
ScrollableTabsContextValue | undefined
>(undefined);
export function useScrollableTabs() {
const context = useContext(ScrollableTabsContext);
if (!context) {
throw new Error("useScrollableTabs must be used within a ScrollableTabs");
}
return context;
}

View File

@@ -0,0 +1,48 @@
import * as React from "react";
const HEADER_OFFSET = 100;
export function calculateScrollPosition(
elementRect: DOMRect,
containerRect: DOMRect,
currentScrollTop: number,
): number {
const elementTopRelativeToContainer =
elementRect.top - containerRect.top + currentScrollTop - HEADER_OFFSET;
return Math.max(0, elementTopRelativeToContainer);
}
function hasDisplayName(
type: unknown,
displayName: string,
): type is { displayName: string } {
return (
typeof type === "object" &&
type !== null &&
"displayName" in type &&
(type as { displayName: unknown }).displayName === displayName
);
}
export function findListElement(
children: React.ReactNode[],
): React.ReactElement | undefined {
return children.find(
(child) =>
React.isValidElement(child) &&
hasDisplayName(child.type, "ScrollableTabsList"),
) as React.ReactElement | undefined;
}
export function findContentElements(
children: React.ReactNode[],
): React.ReactNode[] {
return children.filter(
(child) =>
!(
React.isValidElement(child) &&
hasDisplayName(child.type, "ScrollableTabsList")
),
);
}

View File

@@ -0,0 +1,60 @@
import { useCallback, useRef, useState } from "react";
import { calculateScrollPosition } from "./helpers";
interface Args {
defaultValue?: string;
}
export function useScrollableTabsInternal({ defaultValue }: Args) {
const [activeValue, setActiveValue] = useState<string | null>(
defaultValue || null,
);
const contentRefs = useRef<Map<string, HTMLElement>>(new Map());
const contentContainerRef = useRef<HTMLDivElement | null>(null);
function registerContent(value: string, element: HTMLElement | null) {
if (element) {
contentRefs.current.set(value, element);
} else {
contentRefs.current.delete(value);
}
}
function scrollToSection(value: string) {
const element = contentRefs.current.get(value);
const scrollContainer = contentContainerRef.current;
if (!element || !scrollContainer) return;
setActiveValue(value);
const containerRect = scrollContainer.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const currentScrollTop = scrollContainer.scrollTop;
const scrollTop = calculateScrollPosition(
elementRect,
containerRect,
currentScrollTop,
);
const maxScrollTop =
scrollContainer.scrollHeight - scrollContainer.clientHeight;
const clampedScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
scrollContainer.scrollTo({
top: clampedScrollTop,
behavior: "smooth",
});
}
const memoizedRegisterContent = useCallback(registerContent, []);
const memoizedScrollToSection = useCallback(scrollToSection, []);
return {
activeValue,
setActiveValue,
registerContent: memoizedRegisterContent,
scrollToSection: memoizedScrollToSection,
scrollContainer: contentContainerRef.current,
contentContainerRef,
};
}

View File

@@ -23,6 +23,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip"; } from "@/components/atoms/Tooltip/BaseTooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { BlockUIType } from "@/app/(platform)/build/components/types";
type TypeOption = { type TypeOption = {
type: string; type: string;
@@ -47,7 +48,14 @@ export const AnyOfField = ({
onBlur, onBlur,
onFocus, onFocus,
}: FieldProps) => { }: FieldProps) => {
const handleId = generateHandleId(idSchema.$id ?? ""); const handleId =
formContext.uiType === BlockUIType.AGENT
? (idSchema.$id ?? "")
.split("_")
.filter((p) => p !== "root" && p !== "properties" && p.length > 0)
.join("_") || ""
: generateHandleId(idSchema.$id ?? "");
const updatedFormContexrt = { ...formContext, fromAnyOf: true }; const updatedFormContexrt = { ...formContext, fromAnyOf: true };
const { nodeId, showHandles = true } = updatedFormContexrt; const { nodeId, showHandles = true } = updatedFormContexrt;

View File

@@ -58,7 +58,15 @@ const FieldTemplate: React.FC<FieldTemplateProps> = ({
let handleId = null; let handleId = null;
if (!isArrayItem) { if (!isArrayItem) {
handleId = generateHandleId(fieldId); if (uiType === BlockUIType.AGENT) {
const parts = fieldId.split("_");
const filtered = parts.filter(
(p) => p !== "root" && p !== "properties" && p.length > 0,
);
handleId = filtered.join("_") || "";
} else {
handleId = generateHandleId(fieldId);
}
} else { } else {
handleId = arrayFieldHandleId; handleId = arrayFieldHandleId;
} }

View File

@@ -910,7 +910,37 @@ export default class BackendAPI {
reject(new Error("Invalid JSON response")); reject(new Error("Invalid JSON response"));
} }
} else { } else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); // Handle file size errors with user-friendly message
if (xhr.status === 413) {
reject(new Error("File is too large — max size is 256MB"));
return;
}
// Try to parse error response for better messages
let errorMessage = `Upload failed (${xhr.status})`;
try {
const errorData = JSON.parse(xhr.responseText);
if (errorData.detail) {
if (
typeof errorData.detail === "string" &&
errorData.detail.includes("exceeds the maximum")
) {
const match = errorData.detail.match(
/maximum allowed size of (\d+)MB/,
);
const maxSize = match ? match[1] : "256";
errorMessage = `File is too large — max size is ${maxSize}MB`;
} else if (typeof errorData.detail === "string") {
errorMessage = errorData.detail;
}
} else if (errorData.error) {
errorMessage = errorData.error;
}
} catch {
// Keep default message if parsing fails
}
reject(new Error(errorMessage));
} }
}); });

View File

@@ -184,6 +184,11 @@ export function serializeRequestBody(
} }
export async function parseApiError(response: Response): Promise<string> { export async function parseApiError(response: Response): Promise<string> {
// Handle 413 Payload Too Large with user-friendly message
if (response.status === 413) {
return "File is too large — max size is 256MB";
}
try { try {
const errorData = await response.clone().json(); const errorData = await response.clone().json();
@@ -205,6 +210,16 @@ export async function parseApiError(response: Response): Promise<string> {
return response.statusText; // Fallback to status text if no message return response.statusText; // Fallback to status text if no message
} }
// Check for file size error from backend
if (
typeof errorData.detail === "string" &&
errorData.detail.includes("exceeds the maximum")
) {
const match = errorData.detail.match(/maximum allowed size of (\d+)MB/);
const maxSize = match ? match[1] : "256";
return `File is too large — max size is ${maxSize}MB`;
}
return errorData.detail || errorData.error || response.statusText; return errorData.detail || errorData.error || response.statusText;
} catch { } catch {
return response.statusText; return response.statusText;

View File

@@ -48,7 +48,7 @@ const mockFlags = {
[Flag.AGENT_FAVORITING]: false, [Flag.AGENT_FAVORITING]: false,
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS, [Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
[Flag.ENABLE_PLATFORM_PAYMENT]: false, [Flag.ENABLE_PLATFORM_PAYMENT]: false,
[Flag.CHAT]: true, [Flag.CHAT]: false,
}; };
export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null { export function useGetFlag<T extends Flag>(flag: T): FlagValues[T] | null {

View File

@@ -1,10 +1,10 @@
import scrollbar from "tailwind-scrollbar";
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate"; import tailwindcssAnimate from "tailwindcss-animate";
import scrollbar from "tailwind-scrollbar";
import { colors } from "./src/components/styles/colors"; import { colors } from "./src/components/styles/colors";
const config = { const config = {
darkMode: ["class"], darkMode: ["class", ".dark-mode"], // ignore dark: prefix classes for now until we fully support dark mode
content: ["./src/**/*.{ts,tsx}"], content: ["./src/**/*.{ts,tsx}"],
prefix: "", prefix: "",
theme: { theme: {