Compare commits

...

40 Commits

Author SHA1 Message Date
Waleed
18b7032494 v0.5.24: agent tool and UX improvements, redis service overhaul (#2291)
* feat(folders): add the ability to create a folder within a folder in popover (#2287)

* fix(agent): filter out empty params to ensure LLM can set tool params at runtime (#2288)

* fix(mcp): added backfill effect to add missing descriptions for mcp tools (#2290)

* fix(redis): cleanup access pattern across callsites (#2289)

* fix(redis): cleanup access pattern across callsites

* swap redis command to be non blocking

* improvement(log-details): polling, trace spans (#2292)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2025-12-10 13:09:21 -08:00
Emir Karabeg
76bc2fae83 improvement(log-details): polling, trace spans (#2292) 2025-12-10 13:08:24 -08:00
Vikhyath Mondreti
1cfe229056 fix(redis): cleanup access pattern across callsites (#2289)
* fix(redis): cleanup access pattern across callsites

* swap redis command to be non blocking
2025-12-10 12:37:33 -08:00
Waleed
6791d968b8 fix(mcp): added backfill effect to add missing descriptions for mcp tools (#2290) 2025-12-10 11:57:18 -08:00
Waleed
163db5cf50 fix(agent): filter out empty params to ensure LLM can set tool params at runtime (#2288) 2025-12-10 11:38:54 -08:00
Waleed
bbbb13af7e feat(folders): add the ability to create a folder within a folder in popover (#2287) 2025-12-10 11:19:57 -08:00
Waleed
b7bbef8620 v0.5.23: kb, logs, general ui improvements, token bucket rate limits, docs, mcp, autolayout improvements (#2286)
* fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead (#2273)

* fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead

* added backfill, added loading state for tools in settings > mcp

* fix tool inp

* feat(rate-limiter): token bucket algorithm  (#2270)

* fix(ratelimit): make deployed chat rate limited

* improvement(rate-limiter): use token bucket algo

* update docs

* fix

* fix type

* fix db rate limiter

* address greptile comments

* feat(i18n): update translations (#2275)

Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>

* fix(tools): updated kalshi and polymarket tools to accurately reflect outputs (#2274)

* feat(i18n): update translations (#2276)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(autolayout): align by handle (#2277)

* fix(autolayout): align by handle

* use shared constants everywhere

* cleanup

* fix(copilot): fix custom tools (#2278)

* Fix title custom tool

* Checkpoitn (broken)

* Fix custom tool flash

* Edit workflow returns null fix

* Works

* Fix lint

* fix(ime): prevent form submission during IME composition steps (#2279)

* fix(ui): prevent form submission during IME composition steps

* chore(gitignore): add IntelliJ IDE files to .gitignore

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* feat(ui): logs, kb, emcn (#2207)

* feat(kb): emcn alignment; sidebar: popover primary; settings-modal: expand

* feat: EMCN breadcrumb; improvement(KB): UI

* fix: hydration error

* improvement(KB): UI

* feat: emcn modal sizing, KB tags; refactor: deleted old sidebar

* feat(logs): UI

* fix: add documents modal name

* feat: logs, emcn, cursorrules; refactor: logs

* feat: dashboard

* feat: notifications; improvement: logs details

* fixed random rectangle on canvas

* fixed the name of the file to align

* fix build

---------

Co-authored-by: waleed <walif6@gmail.com>

* fix(creds): glitch allowing multiple credentials in an integration (#2282)

* improvement: custom tools modal, logs-details (#2283)

* fix(docs): fix copy page button and header hook (#2284)

* improvement(chat): add the ability to download files from the deployed chat (#2280)

* added teams download and chat download file

* Removed comments

* removed comments

* component structure and download all

* removed comments

* cleanup code

* fix empty files case

* small fix

* fix(container): resize heuristic improvement (#2285)

* estimate block height for resize based on subblocks

* fix hydration error

* make more conservative

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: mosa <mosaxiv@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
2025-12-10 00:57:58 -08:00
Vikhyath Mondreti
6fd4087a79 fix(container): resize heuristic improvement (#2285)
* estimate block height for resize based on subblocks

* fix hydration error

* make more conservative
2025-12-10 00:20:03 -08:00
Adam Gough
cb6e763714 improvement(chat): add the ability to download files from the deployed chat (#2280)
* added teams download and chat download file

* Removed comments

* removed comments

* component structure and download all

* removed comments

* cleanup code

* fix empty files case

* small fix
2025-12-09 22:08:03 -08:00
Waleed
d06b360b1d fix(docs): fix copy page button and header hook (#2284) 2025-12-09 21:58:54 -08:00
Emir Karabeg
0713580862 improvement: custom tools modal, logs-details (#2283) 2025-12-09 21:50:09 -08:00
Vikhyath Mondreti
f421f27d3f fix(creds): glitch allowing multiple credentials in an integration (#2282) 2025-12-09 21:07:14 -08:00
Emir Karabeg
0083c89fa5 feat(ui): logs, kb, emcn (#2207)
* feat(kb): emcn alignment; sidebar: popover primary; settings-modal: expand

* feat: EMCN breadcrumb; improvement(KB): UI

* fix: hydration error

* improvement(KB): UI

* feat: emcn modal sizing, KB tags; refactor: deleted old sidebar

* feat(logs): UI

* fix: add documents modal name

* feat: logs, emcn, cursorrules; refactor: logs

* feat: dashboard

* feat: notifications; improvement: logs details

* fixed random rectangle on canvas

* fixed the name of the file to align

* fix build

---------

Co-authored-by: waleed <walif6@gmail.com>
2025-12-09 20:50:28 -08:00
mosa
3cec449402 fix(ime): prevent form submission during IME composition steps (#2279)
* fix(ui): prevent form submission during IME composition steps

* chore(gitignore): add IntelliJ IDE files to .gitignore

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-09 19:58:33 -08:00
Siddharth Ganesan
c5b3fcb181 fix(copilot): fix custom tools (#2278)
* Fix title custom tool

* Checkpoitn (broken)

* Fix custom tool flash

* Edit workflow returns null fix

* Works

* Fix lint
2025-12-09 17:42:17 -08:00
Vikhyath Mondreti
dd7db6e144 fix(autolayout): align by handle (#2277)
* fix(autolayout): align by handle

* use shared constants everywhere

* cleanup
2025-12-09 16:28:25 -08:00
Waleed
306043eedb feat(i18n): update translations (#2276)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-09 16:20:43 -08:00
Waleed
569598b39e fix(tools): updated kalshi and polymarket tools to accurately reflect outputs (#2274) 2025-12-09 15:35:36 -08:00
Waleed
cc66aa5a3e feat(i18n): update translations (#2275)
Co-authored-by: icecrasher321 <icecrasher321@users.noreply.github.com>
2025-12-09 15:25:03 -08:00
Vikhyath Mondreti
aea32d423f feat(rate-limiter): token bucket algorithm (#2270)
* fix(ratelimit): make deployed chat rate limited

* improvement(rate-limiter): use token bucket algo

* update docs

* fix

* fix type

* fix db rate limiter

* address greptile comments
2025-12-09 14:57:17 -08:00
Waleed
22abf98835 fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead (#2273)
* fix(mcp): prevent redundant MCP server discovery calls at runtime, use cached tool schema instead

* added backfill, added loading state for tools in settings > mcp

* fix tool inp
2025-12-09 12:44:53 -08:00
Waleed
52edbea659 v0.5.22: rss feed trigger, sftp tool, billing fixes, 413 surfacing, copilot improvements 2025-12-09 10:27:36 -08:00
Waleed
aa1d896b38 feat(i18n): update translations (#2268)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-09 00:46:09 -08:00
Vikhyath Mondreti
d480057fd3 fix(migration): migration got removed by force push (#2253) 2025-12-08 14:08:12 -08:00
Waleed
c27c233da0 v0.5.21: google groups, virtualized code viewer, ui, autolayout, docs improvements 2025-12-08 13:10:50 -08:00
Waleed
ebef5f3a27 v0.5.20: google slides, ui fixes, subflow resizing improvements 2025-12-06 15:36:09 -08:00
Vikhyath Mondreti
12c4c2d44f v0.5.19: copilot fix 2025-12-05 15:27:31 -08:00
Vikhyath Mondreti
929a352edb fix(build): added trigger.dev sdk mock to tests (#2216) 2025-12-05 14:26:50 -08:00
Vikhyath Mondreti
6cd078b0fe v0.5.18: ui fixes, nextjs16, workspace notifications, admin APIs, loading improvements, new slack tools 2025-12-05 14:03:09 -08:00
Waleed
31874939ee v0.5.17: modals, billing fixes, bun update, zoom, dropbox, kalshi, polymarket, datadog, ahrefs, gitlab, shopify, ssh, wordpress integrations 2025-12-04 13:29:46 -08:00
Waleed
e157ce5fbc v0.5.16: MCP fixes, code refactors, jira fixes, new mistral models 2025-12-02 22:02:11 -08:00
Vikhyath Mondreti
774e5d585c v0.5.15: add tools, revert subblock prop change 2025-12-01 13:52:12 -08:00
Vikhyath Mondreti
54cc93743f v0.5.14: fix issue with teams, google selectors + cleanup code 2025-12-01 12:39:39 -08:00
Waleed
8c32ad4c0d v0.5.13: polling fixes, generic agent search tool, status page, smtp, sendgrid, linkedin, more tools (#2148)
* feat(tools): added smtp, sendgrid, mailgun, linkedin, fixed permissions in context menu (#2133)

* feat(tools): added twilio sendgrid integration

* feat(tools): added smtp, sendgrid, mailgun, fixed permissions in context menu

* added top level mocks for sporadically failing tests

* incr type safety

* fix(team-plans): track departed member usage so value not lost (#2118)

* fix(team-plans): track departed member usage so value not lost

* reset usage to 0 when they leave team

* prep merge with stagig

* regen migrations

* fix org invite + ws selection'

---------

Co-authored-by: Waleed <walif6@gmail.com>

* feat(i18n): update translations (#2134)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* feat(creators): add verification for creators (#2135)

* feat(tools): added apify block/tools  (#2136)

* feat(tools): added apify

* cleanup

* feat(i18n): update translations (#2137)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* feat(env): added more optional env var examples (#2138)

* feat(statuspage): added statuspage, updated list of tools in footer, renamed routes (#2139)

* feat(statuspage): added statuspage, updated list of tools in footer, renamed routes

* ack PR comments

* feat(tools): add generic search tool (#2140)

* feat(i18n): update translations (#2141)

* fix(sdks): bump sdk versions (#2142)

* fix(webhooks): count test webhooks towards usage limit (#2143)

* fix(bill): add requestId to webhook processing (#2144)

* improvement(subflow): remove all associated edges when moving a block into a subflow (#2145)

* improvement(subflow): remove all associated edges when moving a block into a subflow

* ack PR comments

* fix(polling): mark webhook failed on webhook trigger errors (#2146)

* fix(deps): declare core transient deps explicitly (#2147)

* fix(deps): declare core transient deps explicitly

* ack PR comments

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-01 10:15:36 -08:00
Waleed
1d08796853 v0.5.12: memory optimizations, sentry, incidentio, posthog, zendesk, pylon, intercom, mailchimp, loading optimizations (#2132)
* fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures (#2115)

* fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures

* ack PR comments

* ack

* improvement(teams-plan): seats increase simplification + not triggering checkout session (#2117)

* improvement(teams-plan): seats increase simplification + not triggering checkout session

* cleanup via helper

* feat(tools): added sentry, incidentio, and posthog tools (#2116)

* feat(tools): added sentry, incidentio, and posthog tools

* update docs

* fixed docs to use native fumadocs for llms.txt and copy markdown, fixed tool issues

* cleanup

* enhance error extractor, fixed posthog tools

* docs enhancements, cleanup

* added more incident io ops, remove zustand/shallow in favor of zustand/react/shallow

* fix type errors

* remove unnecessary comments

* added vllm to docs

* feat(i18n): update translations (#2120)

* feat(i18n): update translations

* fix build

---------

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* improvement(workflow-execution): perf improvements to passing workflow state + decrypted env vars (#2119)

* improvement(execution): load workflow state once instead of 2-3 times

* decrypt only in get helper

* remove comments

* remove comments

* feat(models): host google gemini models (#2122)

* feat(models): host google gemini models

* remove unused primary key

* feat(i18n): update translations (#2123)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* feat(tools): added zendesk, pylon, intercom, & mailchimp (#2126)

* feat(tools): added zendesk, pylon, intercom, & mailchimp

* finish zendesk and pylon

* updated docs

* feat(i18n): update translations (#2129)

* feat(i18n): update translations

* fixed build

---------

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal (#2130)

* fix(permissions): add client-side permissions validation to prevent unauthorized actions, upgraded custom tool modal

* fix failing test

* fix test

* cleanup

* fix(custom-tools): add composite index on custom tool names & workspace id (#2131)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-11-28 16:08:06 -08:00
Waleed
ebcd243942 v0.5.11: stt, videogen, vllm, billing fixes, new models 2025-11-25 01:14:12 -08:00
Waleed
b7e814b721 v0.5.10: copilot upgrade, preprocessor, logs search, UI, code hygiene 2025-11-21 12:04:34 -08:00
Waleed
842ef27ed9 v0.5.9: add backwards compatibility for agent messages array 2025-11-20 11:19:42 -08:00
Vikhyath Mondreti
31c34b2ea3 v0.5.8: notifications, billing, ui changes, store loading state machine 2025-11-20 01:32:32 -08:00
Vikhyath Mondreti
8f0ef58056 v0.5.7: combobox selectors, usage indicator, workflow loading race condition, other improvements 2025-11-17 21:25:51 -08:00
430 changed files with 33552 additions and 22935 deletions

View File

@@ -0,0 +1,45 @@
---
description: EMCN component library patterns with CVA
globs: ["apps/sim/components/emcn/**"]
---
# EMCN Component Guidelines
## When to Use CVA vs Direct Styles
**Use CVA (class-variance-authority) when:**
- 2+ visual variants (primary, secondary, outline)
- Multiple sizes or state variations
- Example: Button with variants
**Use direct className when:**
- Single consistent style
- No variations needed
- Example: Label with one style
## Patterns
**With CVA:**
```tsx
const buttonVariants = cva('base-classes', {
variants: {
variant: { default: '...', primary: '...' },
size: { sm: '...', md: '...' }
}
})
export { Button, buttonVariants }
```
**Without CVA:**
```tsx
function Label({ className, ...props }) {
return <Primitive className={cn('single-style-classes', className)} {...props} />
}
```
## Rules
- Use Radix UI primitives for accessibility
- Export component and variants (if using CVA)
- TSDoc with usage examples
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
- Always use `transition-colors` for hover states

20
.cursor/rules/global.mdc Normal file
View File

@@ -0,0 +1,20 @@
---
description: Global coding standards that apply to all files
alwaysApply: true
---
# Global Standards
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
## Logging
Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
## Comments
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
## Styling
Never update global styles. Keep all styling local to components.
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -0,0 +1,67 @@
---
description: Core architecture principles for the Sim app
globs: ["apps/sim/**"]
---
# Sim App Architecture
## Core Principles
1. **Single Responsibility**: Each component, hook, store has one clear purpose
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
5. **Performance by Default**: useMemo, useCallback, refs appropriately
## File Organization
```
feature/
├── components/ # Feature components
│ └── sub-feature/ # Sub-feature with own components
├── hooks/ # Custom hooks
└── feature.tsx # Main component
```
## Naming Conventions
- **Components**: PascalCase (`WorkflowList`, `TriggerPanel`)
- **Hooks**: camelCase with `use` prefix (`useWorkflowOperations`)
- **Files**: kebab-case matching export (`workflow-list.tsx`)
- **Stores**: kebab-case in stores/ (`sidebar/store.ts`)
- **Constants**: SCREAMING_SNAKE_CASE
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
## State Management
**useState**: UI-only concerns (dropdown open, hover, form inputs)
**Zustand**: Shared state, persistence, global app state
**useRef**: DOM refs, avoiding dependency issues, mutable non-reactive values
## Component Extraction
**Extract to separate file when:**
- Complex (50+ lines)
- Used across 2+ files
- Has own state/logic
**Keep inline when:**
- Simple (< 10 lines)
- Used in only 1 file
- Purely presentational
**Never import utilities from another component file.** Extract shared helpers to `lib/` or `utils/`.
## Utils Files
**Never create a `utils.ts` file for a single consumer.** Inline the logic directly in the consuming component.
**Create `utils.ts` when:**
- 2+ files import the same helper
**Prefer existing sources of truth:**
- Before duplicating logic, check if a centralized helper already exists (e.g., `lib/logs/get-trigger-options.ts`)
- Import from the source of truth rather than creating wrapper functions
**Location hierarchy:**
- `lib/` — App-wide utilities (auth, billing, core)
- `feature/utils.ts` — Feature-scoped utilities (used by 2+ components in the feature)
- Inline — Single-use helpers (define directly in the component)

View File

@@ -0,0 +1,64 @@
---
description: Component patterns and structure for React components
globs: ["apps/sim/**/*.tsx"]
---
# Component Patterns
## Structure Order
```typescript
'use client' // Only if using hooks
// 1. Imports (external → internal → relative)
// 2. Constants at module level
const CONFIG = { SPACING: 8 } as const
// 3. Props interface with TSDoc
interface ComponentProps {
/** Description */
requiredProp: string
optionalProp?: boolean
}
// 4. Component with TSDoc
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
// a. Refs
// b. External hooks (useParams, useRouter)
// c. Store hooks
// d. Custom hooks
// e. Local state
// f. useMemo computations
// g. useCallback handlers
// h. useEffect
// i. Return JSX
}
```
## Rules
1. Add `'use client'` when using React hooks
2. Always define props interface
3. TSDoc on component: description, @param, @returns
4. Extract constants with `as const`
5. Use Tailwind only, no inline styles
6. Semantic HTML (`aside`, `nav`, `article`)
7. Include ARIA attributes where appropriate
8. Optional chain callbacks: `onAction?.(id)`
## Factory Pattern with Caching
When generating components for a specific signature (e.g., icons):
```typescript
const cache = new Map<string, React.ComponentType<{ className?: string }>>()
function getColorIcon(color: string) {
if (cache.has(color)) return cache.get(color)!
const Icon = ({ className }: { className?: string }) => (
<div className={cn(className, 'rounded-[3px]')} style={{ backgroundColor: color, width: 10, height: 10 }} />
)
Icon.displayName = `ColorIcon(${color})`
cache.set(color, Icon)
return Icon
}
```

View File

@@ -0,0 +1,68 @@
---
description: Custom hook patterns and best practices
globs: ["apps/sim/**/use-*.ts", "apps/sim/**/hooks/**/*.ts"]
---
# Hook Patterns
## Structure
```typescript
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('useFeatureName')
interface UseFeatureProps {
id: string
onSuccess?: (result: Result) => void
}
/**
* Hook description.
* @param props - Configuration
* @returns State and operations
*/
export function useFeature({ id, onSuccess }: UseFeatureProps) {
// 1. Refs for stable dependencies
const idRef = useRef(id)
const onSuccessRef = useRef(onSuccess)
// 2. State
const [data, setData] = useState<Data | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
// 3. Sync refs
useEffect(() => {
idRef.current = id
onSuccessRef.current = onSuccess
}, [id, onSuccess])
// 4. Operations with useCallback
const fetchData = useCallback(async () => {
setIsLoading(true)
try {
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
setData(result)
onSuccessRef.current?.(result)
} catch (err) {
setError(err as Error)
logger.error('Failed', { error: err })
} finally {
setIsLoading(false)
}
}, []) // Empty deps - using refs
// 5. Return grouped by state/operations
return { data, isLoading, error, fetchData }
}
```
## Rules
1. Single responsibility per hook
2. Props interface required
3. TSDoc required
4. Use logger, not console.log
5. Refs for stable callback dependencies
6. Wrap returned functions in useCallback
7. Always try/catch async operations
8. Track loading/error states

View File

@@ -0,0 +1,37 @@
---
description: Import patterns for the Sim application
globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
---
# Import Patterns
## EMCN Components
Import from `@/components/emcn`, never from subpaths like `@/components/emcn/components/modal/modal`.
**Exception**: CSS imports use actual file paths: `import '@/components/emcn/components/code/code.css'`
## Feature Components
Import from central folder indexes, not specific subfolders:
```typescript
// ✅ Correct
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
// ❌ Wrong
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard'
```
## Internal vs External
- **Cross-feature**: Absolute paths through central index
- **Within feature**: Relative paths (`./components/...`, `../utils`)
## Import Order
1. React/core libraries
2. External libraries
3. UI components (`@/components/emcn`, `@/components/ui`)
4. Utilities (`@/lib/...`)
5. Feature imports from indexes
6. Relative imports
7. CSS imports
## Types
Use `type` keyword: `import type { WorkflowLog } from '...'`

View File

@@ -0,0 +1,57 @@
---
description: Zustand store patterns
globs: ["apps/sim/**/store.ts", "apps/sim/**/stores/**/*.ts"]
---
# Zustand Store Patterns
## Structure
```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FeatureState {
// State
items: Item[]
activeId: string | null
// Actions
setItems: (items: Item[]) => void
addItem: (item: Item) => void
clearState: () => void
}
const createInitialState = () => ({
items: [],
activeId: null,
})
export const useFeatureStore = create<FeatureState>()(
persist(
(set) => ({
...createInitialState(),
setItems: (items) => set({ items }),
addItem: (item) => set((state) => ({
items: [...state.items, item],
})),
clearState: () => set(createInitialState()),
}),
{
name: 'feature-state',
partialize: (state) => ({ items: state.items }),
}
)
)
```
## Rules
1. Interface includes state and actions
2. Extract config to module constants
3. TSDoc on store
4. Only persist what's needed
5. Immutable updates only - never mutate
6. Use `set((state) => ...)` when depending on previous state
7. Provide clear/reset actions

View File

@@ -0,0 +1,47 @@
---
description: Tailwind CSS and styling conventions
globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
---
# Styling Rules
## Tailwind
1. **No inline styles** - Use Tailwind classes exclusively
2. **No duplicate dark classes** - Don't add `dark:` when value matches light mode
3. **Exact values** - Use design system values (`text-[14px]`, `h-[25px]`)
4. **Prefer px** - Use `px-[4px]` over `px-1`
5. **Transitions** - Add `transition-colors` for interactive states
## Conditional Classes
```typescript
import { cn } from '@/lib/utils'
<div className={cn(
'base-classes',
isActive && 'active-classes',
disabled ? 'opacity-60' : 'hover:bg-accent'
)} />
```
## CSS Variables for Dynamic Styles
```typescript
// In store setter
setSidebarWidth: (width) => {
set({ sidebarWidth: width })
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
}
// In component
<aside style={{ width: 'var(--sidebar-width)' }} />
```
## Anti-Patterns
```typescript
// ❌ Bad
<div style={{ width: 200 }}>
<div className='text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
// ✅ Good
<div className='w-[200px]'>
<div className='text-[var(--text-primary)]'>
```

View File

@@ -0,0 +1,24 @@
---
description: TypeScript conventions and type safety
globs: ["apps/sim/**/*.ts", "apps/sim/**/*.tsx"]
---
# TypeScript Rules
1. **No `any`** - Use proper types or `unknown` with type guards
2. **Props interface** - Always define, even for simple components
3. **Callback types** - Full signature with params and return type
4. **Generics** - Use for reusable components/hooks
5. **Const assertions** - `as const` for constant objects/arrays
6. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
## Anti-Patterns
```typescript
// ❌ Bad
const handleClick = (e: any) => {}
useEffect(() => { doSomething(prop) }, []) // Missing dep
// ✅ Good
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
useEffect(() => { doSomething(prop) }, [prop])
```

View File

@@ -1,19 +0,0 @@
# Role
You are a professional software engineer. All code you write MUST follow best practices, ensuring accuracy, quality, readability, and cleanliness. You MUST make FOCUSED EDITS that are EFFICIENT and ELEGANT.
## Logs
ENSURE that you use the logger.info and logger.warn and logger.error instead of the console.log whenever you want to display logs.
## Comments
You must use TSDOC for comments. Do not use ==== for comments to separate sections. Do not leave any comments that are not TSDOC.
## Globals styles
You should not update the global styles unless it is absolutely necessary. Keep all styling local to components and files.
## Bun
Use bun and bunx not npm and npx

3
.gitignore vendored
View File

@@ -67,6 +67,9 @@ start-collector.sh
# VSCode
.vscode
# IntelliJ
.idea
## Helm Chart Tests
helm/sim/test
i18n.cache

47
CLAUDE.md Normal file
View File

@@ -0,0 +1,47 @@
# Expert Programming Standards
**You are tasked with implementing solutions that follow best practices. You MUST be accurate, elegant, and efficient as an expert programmer.**
---
# Role
You are a professional software engineer. All code you write MUST follow best practices, ensuring accuracy, quality, readability, and cleanliness. You MUST make FOCUSED EDITS that are EFFICIENT and ELEGANT.
## Logs
ENSURE that you use the logger.info and logger.warn and logger.error instead of the console.log whenever you want to display logs.
## Comments
You must use TSDOC for comments. Do not use ==== for comments to separate sections. Do not leave any comments that are not TSDOC.
## Global Styles
You should not update the global styles unless it is absolutely necessary. Keep all styling local to components and files.
## Bun
Use bun and bunx not npm and npx.
## Code Quality
- Write clean, maintainable code that follows the project's existing patterns
- Prefer composition over inheritance
- Keep functions small and focused on a single responsibility
- Use meaningful variable and function names
- Handle errors gracefully and provide useful error messages
- Write type-safe code with proper TypeScript types
## Testing
- Write tests for new functionality when appropriate
- Ensure existing tests pass before completing work
- Follow the project's testing conventions
## Performance
- Consider performance implications of your code
- Avoid unnecessary re-renders in React components
- Use appropriate data structures and algorithms
- Profile and optimize when necessary

View File

@@ -6,9 +6,10 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { PageNavigationArrows } from '@/components/docs-layout/page-navigation-arrows'
import { TOCFooter } from '@/components/docs-layout/toc-footer'
import { LLMCopyButton } from '@/components/page-actions'
import { StructuredData } from '@/components/structured-data'
import { CodeBlock } from '@/components/ui/code-block'
import { CopyPageButton } from '@/components/ui/copy-page-button'
import { Heading } from '@/components/ui/heading'
import { source } from '@/lib/source'
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
@@ -202,7 +203,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
<div className='relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<CopyPageButton markdownUrl={`${page.url}.mdx`} />
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
@@ -214,6 +215,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
components={{
...defaultMdxComponents,
CodeBlock,
h1: (props) => <Heading as='h1' {...props} />,
h2: (props) => <Heading as='h2' {...props} />,
h3: (props) => <Heading as='h3' {...props} />,
h4: (props) => <Heading as='h4' {...props} />,
h5: (props) => <Heading as='h5' {...props} />,
h6: (props) => <Heading as='h6' {...props} />,
}}
/>
</DocsBody>

View File

@@ -2,6 +2,12 @@
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
/* Prevent overscroll bounce effect on the page */
html,
body {
overscroll-behavior: none;
}
@theme {
--color-fd-primary: #802fff; /* Purple from control-bar component */
--font-geist-sans: var(--font-geist-sans);

View File

@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation'
import { type NextRequest, NextResponse } from 'next/server'
import { i18n } from '@/lib/i18n'
import { getLLMText } from '@/lib/llms'
import { source } from '@/lib/source'
@@ -7,7 +8,16 @@ export const revalidate = false
export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params
const page = source.getPage(slug)
let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage
let pageSlug = slug
if (slug && slug.length > 0 && i18n.languages.includes(slug[0] as typeof lang)) {
lang = slug[0] as typeof lang
pageSlug = slug.slice(1)
}
const page = source.getPage(pageSlug, lang)
if (!page) notFound()
return new NextResponse(await getLLMText(page), {

View File

@@ -1,55 +1,50 @@
'use client'
import { useState } from 'react'
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'
import { Check, Copy } from 'lucide-react'
const cache = new Map<string, string>()
interface CopyPageButtonProps {
export function LLMCopyButton({
markdownUrl,
}: {
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl: string
}
export function CopyPageButton({ markdownUrl }: CopyPageButtonProps) {
const [copied, setCopied] = useState(false)
}) {
const [isLoading, setLoading] = useState(false)
const handleCopy = async () => {
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl)
if (cached) {
await navigator.clipboard.writeText(cached)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
return
}
if (cached) return navigator.clipboard.writeText(cached)
setLoading(true)
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)
return content
}),
}),
])
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
} finally {
setLoading(false)
}
}
})
return (
<button
disabled={isLoading}
onClick={handleCopy}
onClick={onClick}
className='flex cursor-pointer items-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-2 text-muted-foreground/60 text-sm leading-none transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label={copied ? 'Copied to clipboard' : 'Copy page content'}
aria-label={checked ? 'Copied to clipboard' : 'Copy page content'}
>
{copied ? (
{checked ? (
<>
<Check className='h-3.5 w-3.5' />
<span>Copied</span>

View File

@@ -0,0 +1,58 @@
'use client'
import { type ComponentPropsWithoutRef, useState } from 'react'
import { Check, Link } from 'lucide-react'
import { cn } from '@/lib/utils'
type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
interface HeadingProps extends ComponentPropsWithoutRef<'h1'> {
as?: HeadingTag
}
export function Heading({ as, className, ...props }: HeadingProps) {
const [copied, setCopied] = useState(false)
const As = as ?? 'h1'
if (!props.id) {
return <As className={className} {...props} />
}
const handleClick = async (e: React.MouseEvent) => {
e.preventDefault()
const url = `${window.location.origin}${window.location.pathname}#${props.id}`
try {
await navigator.clipboard.writeText(url)
setCopied(true)
// Update URL hash without scrolling
window.history.pushState(null, '', `#${props.id}`)
setTimeout(() => setCopied(false), 2000)
} catch {
// Fallback: just navigate to the anchor
window.location.hash = props.id as string
}
}
return (
<As className={cn('group flex scroll-m-28 flex-row items-center gap-2', className)} {...props}>
<a data-card='' href={`#${props.id}`} className='peer' onClick={handleClick}>
{props.children}
</a>
{copied ? (
<Check
aria-hidden
className='size-3.5 shrink-0 text-green-500 opacity-100 transition-opacity'
/>
) : (
<Link
aria-hidden
className='size-3.5 shrink-0 text-fd-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 peer-hover:opacity-100'
/>
)}
</As>
)
}

View File

@@ -27,14 +27,16 @@ Alle API-Antworten enthalten Informationen über Ihre Workflow-Ausführungslimit
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ Alle API-Antworten enthalten Informationen über Ihre Workflow-Ausführungslimit
}
```
**Hinweis:** Die Ratenbegrenzungen in der Antwort beziehen sich auf Workflow-Ausführungen. Die Ratenbegrenzungen für den Aufruf dieses API-Endpunkts befinden sich in den Antwort-Headern (`X-RateLimit-*`).
**Hinweis:** Ratenbegrenzungen verwenden einen Token-Bucket-Algorithmus. `remaining` kann `requestsPerMinute` bis zu `maxBurst` überschreiten, wenn du dein volles Kontingent in letzter Zeit nicht genutzt hast, was Burst-Traffic ermöglicht. Die Ratenbegrenzungen im Antworttext gelten für Workflow-Ausführungen. Die Ratenbegrenzungen für den Aufruf dieses API-Endpunkts befinden sich in den Antwort-Headern (`X-RateLimit-*`).
### Logs abfragen
@@ -110,13 +112,15 @@ Fragen Sie Workflow-Ausführungsprotokolle mit umfangreichen Filteroptionen ab.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -190,13 +194,15 @@ Rufen Sie detaillierte Informationen zu einem bestimmten Logeintrag ab.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -482,19 +488,27 @@ Fehlgeschlagene Webhook-Zustellungen werden mit exponentiellem Backoff und Jitte
## Rate-Limiting
Die API implementiert Rate-Limiting, um eine faire Nutzung zu gewährleisten:
Die API verwendet einen **Token-Bucket-Algorithmus** für die Ratenbegrenzung, der eine faire Nutzung ermöglicht und gleichzeitig Burst-Traffic zulässt:
- **Kostenloser Plan**: 10 Anfragen pro Minute
- **Pro-Plan**: 30 Anfragen pro Minute
- **Team-Plan**: 60 Anfragen pro Minute
- **Enterprise-Plan**: Individuelle Limits
| Plan | Anfragen/Minute | Burst-Kapazität |
|------|-----------------|----------------|
| Free | 10 | 20 |
| Pro | 30 | 60 |
| Team | 60 | 120 |
| Enterprise | 120 | 240 |
Rate-Limit-Informationen sind in den Antwort-Headern enthalten:
- `X-RateLimit-Limit`: Maximale Anfragen pro Zeitfenster
- `X-RateLimit-Remaining`: Verbleibende Anfragen im aktuellen Zeitfenster
- `X-RateLimit-Reset`: ISO-Zeitstempel, wann das Zeitfenster zurückgesetzt wird
**Wie es funktioniert:**
- Tokens werden mit der Rate `requestsPerMinute` aufgefüllt
- Du kannst im Leerlauf bis zu `maxBurst` Tokens ansammeln
- Jede Anfrage verbraucht 1 Token
- Die Burst-Kapazität ermöglicht die Bewältigung von Verkehrsspitzen
## Beispiel: Abfragen neuer Logs
Informationen zur Ratenbegrenzung sind in den Antwort-Headern enthalten:
- `X-RateLimit-Limit`: Anfragen pro Minute (Auffüllrate)
- `X-RateLimit-Remaining`: Aktuell verfügbare Tokens
- `X-RateLimit-Reset`: ISO-Zeitstempel, wann Tokens als nächstes aufgefüllt werden
## Beispiel: Abfragen nach neuen Logs
```javascript
let cursor = null;
@@ -541,7 +555,7 @@ async function pollLogs() {
setInterval(pollLogs, 30000);
```
## Beispiel: Verarbeiten von Webhooks
## Beispiel: Verarbeitung von Webhooks
```javascript
import express from 'express';

View File

@@ -147,8 +147,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -159,49 +171,54 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**Rate-Limit-Felder:**
- `requestsPerMinute`: Dauerhafte Rate-Begrenzung (Tokens werden mit dieser Rate aufgefüllt)
- `maxBurst`: Maximale Tokens, die Sie ansammeln können (Burst-Kapazität)
- `remaining`: Aktuell verfügbare Tokens (können bis zu `maxBurst` sein)
**Antwortfelder:**
- `currentPeriodCost` zeigt die Nutzung im aktuellen Abrechnungszeitraum
- `limit` wird aus individuellen Limits (Free/Pro) oder gebündelten Organisationslimits (Team/Enterprise) abgeleitet
- `currentPeriodCost` spiegelt die Nutzung in der aktuellen Abrechnungsperiode wider
- `limit` wird von individuellen Limits (Free/Pro) oder gepoolten Organisationslimits (Team/Enterprise) abgeleitet
- `plan` ist der aktive Plan mit der höchsten Priorität, der mit Ihrem Benutzer verknüpft ist
## Plan-Limits
Verschiedene Abonnementpläne haben unterschiedliche Nutzungslimits:
| Plan | Monatliches Nutzungslimit | Ratengrenze (pro Minute) |
| Plan | Monatliches Nutzungslimit | Rate-Limits (pro Minute) |
|------|-------------------|-------------------------|
| **Free** | $10 | 5 sync, 10 async |
| **Pro** | $100 | 10 sync, 50 async |
| **Team** | $500 (gebündelt) | 50 sync, 100 async |
| **Team** | $500 (gepoolt) | 50 sync, 100 async |
| **Enterprise** | Individuell | Individuell |
## Abrechnungsmodell
Sim verwendet ein **Basisabonnement + Überschreitung** Abrechnungsmodell:
Sim verwendet ein **Basisabonnement + Mehrverbrauch**-Abrechnungsmodell:
### Wie es funktioniert
**Pro Plan ($20/Monat):**
**Pro-Plan ($20/Monat):**
- Monatliches Abonnement beinhaltet $20 Nutzung
- Nutzung unter $20 → Keine zusätzlichen Kosten
- Nutzung über $20 → Zahlung der Überschreitung am Monatsende
- Beispiel: $35 Nutzung = $20 (Abonnement) + $15 (Überschreitung)
- Nutzung über $20 → Zahlen Sie den Mehrverbrauch am Monatsende
- Beispiel: $35 Nutzung = $20 (Abonnement) + $15 (Mehrverbrauch)
**Team Plan ($40/Benutzer/Monat):**
- Gebündelte Nutzung für alle Teammitglieder
- Überschreitung wird aus der Gesamtnutzung des Teams berechnet
**Team-Plan ($40/Benutzer/Monat):**
- Gepoolte Nutzung für alle Teammitglieder
- Mehrverbrauch wird aus der Gesamtnutzung des Teams berechnet
- Organisationsinhaber erhält eine Rechnung
**Enterprise Pläne:**
- Fester monatlicher Preis, keine Überschreitungen
**Enterprise-Pläne:**
- Fester monatlicher Preis, kein Mehrverbrauch
- Individuelle Nutzungslimits gemäß Vereinbarung
### Schwellenwertabrechnung
### Schwellenwert-Abrechnung
Wenn die nicht abgerechnete Überschreitung $50 erreicht, berechnet Sim automatisch den gesamten nicht abgerechneten Betrag.
Wenn der nicht abgerechnete Mehrverbrauch $50 erreicht, berechnet Sim automatisch den gesamten nicht abgerechneten Betrag.
**Beispiel:**
- Tag 10: $70 Überschreitung → Sofortige Abrechnung von $70
- Tag 10: $70 Mehrverbrauch → Sofortige Abrechnung von $70
- Tag 15: Zusätzliche $35 Nutzung ($105 insgesamt) → Bereits abgerechnet, keine Aktion
- Tag 20: Weitere $50 Nutzung ($155 insgesamt, $85 nicht abgerechnet) → Sofortige Abrechnung von $85

View File

@@ -49,8 +49,8 @@ Rufe eine Liste von Prognosemärkten von Kalshi mit optionaler Filterung ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Marktdaten und Metadaten |
| `markets` | array | Array von Markt-Objekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_market`
@@ -66,8 +66,7 @@ Rufe Details eines bestimmten Prognosemarkts nach Ticker ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Marktdaten und Metadaten |
| `market` | object | Markt-Objekt mit Details |
### `kalshi_get_events`
@@ -85,10 +84,10 @@ Rufe eine Liste von Events von Kalshi mit optionaler Filterung ab
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Ereignisdaten und Metadaten |
| `events` | array | Array von Ereignis-Objekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_event`
@@ -103,10 +102,9 @@ Details eines bestimmten Ereignisses anhand des Tickers abrufen
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Ereignisdaten und Metadaten |
| `event` | object | Ereignis-Objekt mit Details |
### `kalshi_get_balance`
@@ -121,10 +119,12 @@ Kontostand und Portfoliowert von Kalshi abrufen
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Kontostandsdaten und Metadaten |
| `balance` | number | Kontostand in Cent |
| `portfolioValue` | number | Portfoliowert in Cent |
| `balanceDollars` | number | Kontostand in Dollar |
| `portfolioValueDollars` | number | Portfoliowert in Dollar |
### `kalshi_get_positions`
@@ -146,8 +146,8 @@ Offene Positionen von Kalshi abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Positionsdaten und Metadaten |
| `positions` | array | Array von Positions-Objekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_orders`
@@ -169,8 +169,8 @@ Rufen Sie Ihre Bestellungen von Kalshi mit optionaler Filterung ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Bestelldaten und Metadaten |
| `orders` | array | Array von Auftrags-Objekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_order`
@@ -188,8 +188,7 @@ Rufen Sie Details zu einem bestimmten Auftrag anhand der ID von Kalshi ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Auftragsdaten |
| `order` | object | Auftrags-Objekt mit Details |
### `kalshi_get_orderbook`
@@ -205,8 +204,7 @@ Rufen Sie das Orderbuch (Ja- und Nein-Gebote) für einen bestimmten Markt ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Orderbuch-Daten und Metadaten |
| `orderbook` | object | Orderbuch mit Ja/Nein-Geboten und -Anfragen |
### `kalshi_get_trades`
@@ -223,8 +221,8 @@ Rufen Sie aktuelle Trades über alle Märkte hinweg ab
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Handelsdaten und Metadaten |
| `trades` | array | Array von Handelsobjekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_candlesticks`
@@ -244,8 +242,7 @@ OHLC-Kerzendaten für einen bestimmten Markt abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Kerzendaten und Metadaten |
| `candlesticks` | array | Array von OHLC-Kerzendaten |
### `kalshi_get_fills`
@@ -268,8 +265,8 @@ Ihr Portfolio abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Ausführungsdaten und Metadaten |
| `fills` | array | Array von Ausführungs-/Handelsobjekten |
| `paging` | object | Paginierungscursor zum Abrufen weiterer Ergebnisse |
### `kalshi_get_series_by_ticker`
@@ -285,8 +282,7 @@ Details einer bestimmten Marktserie nach Ticker abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Seriendaten und Metadaten |
| `series` | object | Serienobjekt mit Details |
### `kalshi_get_exchange_status`
@@ -301,8 +297,7 @@ Den aktuellen Status der Kalshi-Börse abrufen (Handel und Börsenaktivität)
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Börsenstatus-Daten und Metadaten |
| `status` | object | Börsenstatus mit trading_active und exchange_active Flags |
### `kalshi_create_order`
@@ -336,8 +331,7 @@ Eine neue Order auf einem Kalshi-Prognosemarkt erstellen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Erstellte Auftragsdaten |
| `order` | object | Das erstellte Auftragsobjekt |
### `kalshi_cancel_order`
@@ -355,8 +349,8 @@ Einen bestehenden Auftrag auf Kalshi stornieren
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Stornierte Auftragsdaten |
| `order` | object | Das stornierte Auftragsobjekt |
| `reducedBy` | number | Anzahl der stornierten Kontrakte |
### `kalshi_amend_order`
@@ -384,8 +378,7 @@ Preis oder Menge eines bestehenden Auftrags auf Kalshi ändern
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Geänderte Auftragsdaten |
| `order` | object | Das geänderte Auftragsobjekt |
## Hinweise

View File

@@ -44,14 +44,14 @@ Rufen Sie eine Liste von Prognosemärkten von Polymarket mit optionaler Filterun
| `order` | string | Nein | Sortierfeld \(z.B. volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | Nein | Sortierrichtung \(true für aufsteigend, false für absteigend\) |
| `tagId` | string | Nein | Nach Tag-ID filtern |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Marktdaten und Metadaten |
### `polymarket_get_market`
@@ -80,11 +80,11 @@ Ruft eine Liste von Events von Polymarket mit optionaler Filterung ab
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `closed` | string | Nein | Nach geschlossenem Status filtern \(true/false\). Verwenden Sie false für nur aktive Events. |
| `closed` | string | Nein | Nach geschlossenem Status filtern \(true/false\). Verwenden Sie false für nur aktive Ereignisse. |
| `order` | string | Nein | Sortierfeld \(z.B. volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | Nein | Sortierrichtung \(true für aufsteigend, false für absteigend\) |
| `tagId` | string | Nein | Nach Tag-ID filtern |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
#### Ausgabe
@@ -107,10 +107,10 @@ Ruft Details eines bestimmten Events nach ID oder Slug ab
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Ereignisdaten und Metadaten |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Event-Daten und Metadaten |
### `polymarket_get_tags`
@@ -118,16 +118,16 @@ Verfügbare Tags zum Filtern von Märkten von Polymarket abrufen
#### Input
| Parameter | Type | Erforderlich | Beschreibung |
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Tag-Daten und Metadaten |
### `polymarket_search`
@@ -136,17 +136,17 @@ Nach Märkten, Ereignissen und Profilen auf Polymarket suchen
#### Input
| Parameter | Type | Erforderlich | Beschreibung |
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `query` | string | Ja | Suchbegriff |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset |
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Suchergebnisse und Metadaten |
### `polymarket_get_series`
@@ -155,17 +155,16 @@ Serien (verwandte Marktgruppen) von Polymarket abrufen
#### Input
| Parameter | Type | Erforderlich | Beschreibung |
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Seriendaten und Metadaten |
| `series` | array | Array von Serien-Objekten |
### `polymarket_get_series_by_id`
@@ -179,10 +178,9 @@ Eine bestimmte Serie (zugehörige Marktgruppe) anhand der ID von Polymarket abru
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Seriendaten und Metadaten |
| `series` | object | Serien-Objekt mit Details |
### `polymarket_get_orderbook`
@@ -196,10 +194,9 @@ Die Orderbuch-Zusammenfassung für einen bestimmten Token abrufen
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Orderbuch-Daten und Metadaten |
| `orderbook` | object | Orderbuch mit Geld- und Briefkurs-Arrays |
### `polymarket_get_price`
@@ -216,8 +213,7 @@ Den Marktpreis für einen bestimmten Token und eine bestimmte Seite abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Preisdaten und Metadaten |
| `price` | string | Marktpreis |
### `polymarket_get_midpoint`
@@ -233,8 +229,7 @@ Abrufen des Mittelpreises für einen bestimmten Token
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Mittelpreisdaten und Metadaten |
| `midpoint` | string | Mittelkurs |
### `polymarket_get_price_history`
@@ -254,8 +249,7 @@ Abrufen historischer Preisdaten für einen bestimmten Markttoken
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Preisverlaufsdaten und Metadaten |
| `history` | array | Array von Preisverlaufseinträgen mit Zeitstempel \(t\) und Preis \(p\) |
### `polymarket_get_last_trade_price`
@@ -271,8 +265,7 @@ Den letzten Handelspreis für einen bestimmten Token abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Letzter Handelspreis und Metadaten |
| `price` | string | Letzter Handelspreis |
### `polymarket_get_spread`
@@ -288,8 +281,7 @@ Die Geld-Brief-Spanne für einen bestimmten Token abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Spread-Daten und Metadaten |
| `spread` | object | Geld-Brief-Spanne mit Geld- und Briefkursen |
### `polymarket_get_tick_size`
@@ -305,8 +297,7 @@ Die minimale Tickgröße für einen bestimmten Token abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Tickgröße und Metadaten |
| `tickSize` | string | Minimale Tick-Größe |
### `polymarket_get_positions`
@@ -323,8 +314,7 @@ Benutzerpositionen von Polymarket abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Positionsdaten und Metadaten |
| `positions` | array | Array von Positions-Objekten |
### `polymarket_get_trades`
@@ -336,15 +326,14 @@ Handelshistorie von Polymarket abrufen
| --------- | ---- | -------- | ----------- |
| `user` | string | Nein | Wallet-Adresse des Benutzers zum Filtern von Trades |
| `market` | string | Nein | Markt-ID zum Filtern von Trades |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(empfohlen: 25-50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe so viele Ergebnisse\) |
| `limit` | string | Nein | Anzahl der Ergebnisse pro Seite \(max 50\) |
| `offset` | string | Nein | Paginierungsoffset \(überspringe diese Anzahl an Ergebnissen\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Handelsdaten und Metadaten |
| `trades` | array | Array von Handelsobjekten |
## Hinweise

View File

@@ -342,19 +342,30 @@ Eine E-Mail-Vorlage von SendGrid löschen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `messageId` | string | E-Mail-Nachrichten-ID (send_mail) |
| `message` | string | Status- oder Erfolgsmeldung |
| `messageId` | string | E-Mail-Nachrichten-ID \(send_mail\) |
| `to` | string | E-Mail-Adresse des Empfängers \(send_mail\) |
| `subject` | string | E-Mail-Betreff \(send_mail, create_template_version\) |
| `id` | string | Ressourcen-ID |
| `jobId` | string | Job-ID für asynchrone Operationen |
| `email` | string | E-Mail-Adresse |
| `firstName` | string | Vorname |
| `lastName` | string | Nachname |
| `email` | string | E-Mail-Adresse des Kontakts |
| `firstName` | string | Vorname des Kontakts |
| `lastName` | string | Nachname des Kontakts |
| `createdAt` | string | Erstellungszeitstempel |
| `updatedAt` | string | Zeitstempel der letzten Aktualisierung |
| `listIds` | json | Array von Listen-IDs, zu denen der Kontakt gehört |
| `customFields` | json | Benutzerdefinierte Feldwerte |
| `contacts` | json | Array von Kontakten |
| `contactCount` | number | Anzahl der Kontakte |
| `lists` | json | Array von Listen |
| `templates` | json | Array von Vorlagen |
| `message` | string | Status- oder Erfolgsmeldung |
| `name` | string | Ressourcenname |
| `templates` | json | Array von Vorlagen |
| `generation` | string | Vorlagengeneration |
| `versions` | json | Array von Vorlagenversionen |
| `templateId` | string | Vorlagen-ID |
| `active` | boolean | Ob die Vorlagenversion aktiv ist |
| `htmlContent` | string | HTML-Inhalt |
| `plainContent` | string | Nur-Text-Inhalt |
### `sendgrid_create_template_version`

View File

@@ -30,15 +30,19 @@ Verwende den Start-Block für alles, was aus dem Editor, deploy-to-API oder depl
<Card title="Schedule" href="/triggers/schedule">
Cron- oder intervallbasierte Ausführung
</Card>
<Card title="RSS Feed" href="/triggers/rss">
RSS- und Atom-Feeds auf neue Inhalte überwachen
</Card>
</Cards>
## Schneller Vergleich
| Trigger | Startbedingung |
|---------|-----------------|
| **Start** | Editor-Ausführungen, deploy-to-API Anfragen oder Chat-Nachrichten |
| **Start** | Editor-Ausführungen, Deploy-to-API-Anfragen oder Chat-Nachrichten |
| **Schedule** | Timer, der im Schedule-Block verwaltet wird |
| **Webhook** | Bei eingehender HTTP-Anfrage |
| **RSS Feed** | Neues Element im Feed veröffentlicht |
> Der Start-Block stellt immer `input`, `conversationId` und `files` Felder bereit. Füge benutzerdefinierte Felder zum Eingabeformat für zusätzliche strukturierte Daten hinzu.

View File

@@ -0,0 +1,49 @@
---
title: RSS-Feed
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Der RSS-Feed-Block überwacht RSS- und Atom-Feeds wenn neue Einträge veröffentlicht werden, wird Ihr Workflow automatisch ausgelöst.
<div className="flex justify-center">
<Image
src="/static/blocks/rss.png"
alt="RSS-Feed-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
1. **RSS-Feed-Block hinzufügen** - Ziehen Sie den RSS-Feed-Block, um Ihren Workflow zu starten
2. **Feed-URL eingeben** - Fügen Sie die URL eines beliebigen RSS- oder Atom-Feeds ein
3. **Bereitstellen** - Stellen Sie Ihren Workflow bereit, um das Polling zu aktivieren
Nach der Bereitstellung wird der Feed jede Minute auf neue Einträge überprüft.
## Ausgabefelder
| Feld | Typ | Beschreibung |
|-------|------|-------------|
| `title` | string | Titel des Eintrags |
| `link` | string | Link des Eintrags |
| `pubDate` | string | Veröffentlichungsdatum |
| `item` | object | Rohdaten des Eintrags mit allen Feldern |
| `feed` | object | Rohdaten der Feed-Metadaten |
Greifen Sie direkt auf zugeordnete Felder zu (`<rss.title>`) oder verwenden Sie die Rohobjekte für beliebige Felder (`<rss.item.author>`, `<rss.feed.language>`).
## Anwendungsfälle
- **Inhaltsüberwachung** - Verfolgen Sie Blogs, Nachrichtenseiten oder Updates von Wettbewerbern
- **Podcast-Automatisierung** - Lösen Sie Workflows aus, wenn neue Episoden erscheinen
- **Release-Tracking** - Überwachen Sie GitHub-Releases, Changelogs oder Produkt-Updates
- **Social-Media-Aggregation** - Sammeln Sie Inhalte von Plattformen, die RSS-Feeds anbieten
<Callout>
RSS-Trigger werden nur für Einträge ausgelöst, die nach dem Speichern des Triggers veröffentlicht wurden. Bestehende Feed-Einträge werden nicht verarbeitet.
</Callout>

View File

@@ -27,14 +27,16 @@ All API responses include information about your workflow execution limits and u
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ All API responses include information about your workflow execution limits and u
}
```
**Note:** The rate limits in the response body are for workflow executions. The rate limits for calling this API endpoint are in the response headers (`X-RateLimit-*`).
**Note:** Rate limits use a token bucket algorithm. `remaining` can exceed `requestsPerMinute` up to `maxBurst` when you haven't used your full allowance recently, allowing for burst traffic. The rate limits in the response body are for workflow executions. The rate limits for calling this API endpoint are in the response headers (`X-RateLimit-*`).
### Query Logs
@@ -108,13 +110,15 @@ Query workflow execution logs with extensive filtering options.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -184,13 +188,15 @@ Retrieve detailed information about a specific log entry.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -467,17 +473,25 @@ Failed webhook deliveries are retried with exponential backoff and jitter:
## Rate Limiting
The API implements rate limiting to ensure fair usage:
The API uses a **token bucket algorithm** for rate limiting, providing fair usage while allowing burst traffic:
- **Free plan**: 10 requests per minute
- **Pro plan**: 30 requests per minute
- **Team plan**: 60 requests per minute
- **Enterprise plan**: Custom limits
| Plan | Requests/Minute | Burst Capacity |
|------|-----------------|----------------|
| Free | 10 | 20 |
| Pro | 30 | 60 |
| Team | 60 | 120 |
| Enterprise | 120 | 240 |
**How it works:**
- Tokens refill at `requestsPerMinute` rate
- You can accumulate up to `maxBurst` tokens when idle
- Each request consumes 1 token
- Burst capacity allows handling traffic spikes
Rate limit information is included in response headers:
- `X-RateLimit-Limit`: Maximum requests per window
- `X-RateLimit-Remaining`: Requests remaining in current window
- `X-RateLimit-Reset`: ISO timestamp when the window resets
- `X-RateLimit-Limit`: Requests per minute (refill rate)
- `X-RateLimit-Remaining`: Current tokens available
- `X-RateLimit-Reset`: ISO timestamp when tokens next refill
## Example: Polling for New Logs

View File

@@ -143,8 +143,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -155,6 +167,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**Rate Limit Fields:**
- `requestsPerMinute`: Sustained rate limit (tokens refill at this rate)
- `maxBurst`: Maximum tokens you can accumulate (burst capacity)
- `remaining`: Current tokens available (can be up to `maxBurst`)
**Response Fields:**
- `currentPeriodCost` reflects usage in the current billing period
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise)

View File

@@ -52,8 +52,8 @@ Retrieve a list of prediction markets from Kalshi with optional filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Markets data and metadata |
| `markets` | array | Array of market objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_market`
@@ -69,8 +69,7 @@ Retrieve details of a specific prediction market by ticker
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Market data and metadata |
| `market` | object | Market object with details |
### `kalshi_get_events`
@@ -90,8 +89,8 @@ Retrieve a list of events from Kalshi with optional filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Events data and metadata |
| `events` | array | Array of event objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_event`
@@ -108,8 +107,7 @@ Retrieve details of a specific event by ticker
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Event data and metadata |
| `event` | object | Event object with details |
### `kalshi_get_balance`
@@ -126,8 +124,10 @@ Retrieve your account balance and portfolio value from Kalshi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Balance data and metadata |
| `balance` | number | Account balance in cents |
| `portfolioValue` | number | Portfolio value in cents |
| `balanceDollars` | number | Account balance in dollars |
| `portfolioValueDollars` | number | Portfolio value in dollars |
### `kalshi_get_positions`
@@ -149,8 +149,8 @@ Retrieve your open positions from Kalshi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Positions data and metadata |
| `positions` | array | Array of position objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_orders`
@@ -172,8 +172,8 @@ Retrieve your orders from Kalshi with optional filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Orders data and metadata |
| `orders` | array | Array of order objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_order`
@@ -191,8 +191,7 @@ Retrieve details of a specific order by ID from Kalshi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Order data |
| `order` | object | Order object with details |
### `kalshi_get_orderbook`
@@ -208,8 +207,7 @@ Retrieve the orderbook (yes and no bids) for a specific market
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Orderbook data and metadata |
| `orderbook` | object | Orderbook with yes/no bids and asks |
### `kalshi_get_trades`
@@ -226,8 +224,8 @@ Retrieve recent trades across all markets
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Trades data and metadata |
| `trades` | array | Array of trade objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_candlesticks`
@@ -247,8 +245,7 @@ Retrieve OHLC candlestick data for a specific market
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Candlestick data and metadata |
| `candlesticks` | array | Array of OHLC candlestick data |
### `kalshi_get_fills`
@@ -271,8 +268,8 @@ Retrieve your portfolio
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Fills data and metadata |
| `fills` | array | Array of fill/trade objects |
| `paging` | object | Pagination cursor for fetching more results |
### `kalshi_get_series_by_ticker`
@@ -288,8 +285,7 @@ Retrieve details of a specific market series by ticker
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Series data and metadata |
| `series` | object | Series object with details |
### `kalshi_get_exchange_status`
@@ -304,8 +300,7 @@ Retrieve the current status of the Kalshi exchange (trading and exchange activit
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Exchange status data and metadata |
| `status` | object | Exchange status with trading_active and exchange_active flags |
### `kalshi_create_order`
@@ -339,8 +334,7 @@ Create a new order on a Kalshi prediction market
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created order data |
| `order` | object | The created order object |
### `kalshi_cancel_order`
@@ -358,8 +352,8 @@ Cancel an existing order on Kalshi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Canceled order data |
| `order` | object | The canceled order object |
| `reducedBy` | number | Number of contracts canceled |
### `kalshi_amend_order`
@@ -387,8 +381,7 @@ Modify the price or quantity of an existing order on Kalshi
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Amended order data |
| `order` | object | The amended order object |

View File

@@ -47,15 +47,14 @@ Retrieve a list of prediction markets from Polymarket with optional filtering
| `order` | string | No | Sort field \(e.g., volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) |
| `tagId` | string | No | Filter by tag ID |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Markets data and metadata |
| `markets` | array | Array of market objects |
### `polymarket_get_market`
@@ -72,8 +71,7 @@ Retrieve details of a specific prediction market by ID or slug
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Market data and metadata |
| `market` | object | Market object with details |
### `polymarket_get_events`
@@ -87,15 +85,14 @@ Retrieve a list of events from Polymarket with optional filtering
| `order` | string | No | Sort field \(e.g., volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) |
| `tagId` | string | No | Filter by tag ID |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Events data and metadata |
| `events` | array | Array of event objects |
### `polymarket_get_event`
@@ -112,8 +109,7 @@ Retrieve details of a specific event by ID or slug
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Event data and metadata |
| `event` | object | Event object with details |
### `polymarket_get_tags`
@@ -123,15 +119,14 @@ Retrieve available tags for filtering markets from Polymarket
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Tags data and metadata |
| `tags` | array | Array of tag objects with id, label, and slug |
### `polymarket_search`
@@ -142,15 +137,14 @@ Search for markets, events, and profiles on Polymarket
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | Search query term |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search results and metadata |
| `results` | object | Search results containing markets, events, and profiles arrays |
### `polymarket_get_series`
@@ -160,15 +154,14 @@ Retrieve series (related market groups) from Polymarket
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Series data and metadata |
| `series` | array | Array of series objects |
### `polymarket_get_series_by_id`
@@ -184,8 +177,7 @@ Retrieve a specific series (related market group) by ID from Polymarket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Series data and metadata |
| `series` | object | Series object with details |
### `polymarket_get_orderbook`
@@ -201,8 +193,7 @@ Retrieve the order book summary for a specific token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Orderbook data and metadata |
| `orderbook` | object | Order book with bids and asks arrays |
### `polymarket_get_price`
@@ -219,8 +210,7 @@ Retrieve the market price for a specific token and side
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Price data and metadata |
| `price` | string | Market price |
### `polymarket_get_midpoint`
@@ -236,8 +226,7 @@ Retrieve the midpoint price for a specific token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Midpoint price data and metadata |
| `midpoint` | string | Midpoint price |
### `polymarket_get_price_history`
@@ -257,8 +246,7 @@ Retrieve historical price data for a specific market token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Price history data and metadata |
| `history` | array | Array of price history entries with timestamp \(t\) and price \(p\) |
### `polymarket_get_last_trade_price`
@@ -274,8 +262,7 @@ Retrieve the last trade price for a specific token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Last trade price and metadata |
| `price` | string | Last trade price |
### `polymarket_get_spread`
@@ -291,8 +278,7 @@ Retrieve the bid-ask spread for a specific token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Spread data and metadata |
| `spread` | object | Bid-ask spread with bid and ask prices |
### `polymarket_get_tick_size`
@@ -308,8 +294,7 @@ Retrieve the minimum tick size for a specific token
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Tick size and metadata |
| `tickSize` | string | Minimum tick size |
### `polymarket_get_positions`
@@ -326,8 +311,7 @@ Retrieve user positions from Polymarket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Positions data and metadata |
| `positions` | array | Array of position objects |
### `polymarket_get_trades`
@@ -339,15 +323,14 @@ Retrieve trade history from Polymarket
| --------- | ---- | -------- | ----------- |
| `user` | string | No | User wallet address to filter trades |
| `market` | string | No | Market ID to filter trades |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
| `limit` | string | No | Number of results per page \(max 50\) |
| `offset` | string | No | Pagination offset \(skip this many results\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Trades data and metadata |
| `trades` | array | Array of trade objects |

View File

@@ -345,19 +345,30 @@ Delete an email template from SendGrid
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `message` | string | Status or success message |
| `messageId` | string | Email message ID \(send_mail\) |
| `to` | string | Recipient email address \(send_mail\) |
| `subject` | string | Email subject \(send_mail, create_template_version\) |
| `id` | string | Resource ID |
| `jobId` | string | Job ID for async operations |
| `email` | string | Email address |
| `firstName` | string | First name |
| `lastName` | string | Last name |
| `email` | string | Contact email address |
| `firstName` | string | Contact first name |
| `lastName` | string | Contact last name |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `listIds` | json | Array of list IDs the contact belongs to |
| `customFields` | json | Custom field values |
| `contacts` | json | Array of contacts |
| `contactCount` | number | Number of contacts |
| `lists` | json | Array of lists |
| `templates` | json | Array of templates |
| `message` | string | Status or success message |
| `name` | string | Resource name |
| `templates` | json | Array of templates |
| `generation` | string | Template generation |
| `versions` | json | Array of template versions |
| `templateId` | string | Template ID |
| `active` | boolean | Whether template version is active |
| `htmlContent` | string | HTML content |
| `plainContent` | string | Plain text content |
### `sendgrid_create_template_version`

View File

@@ -27,14 +27,16 @@ Todas las respuestas de la API incluyen información sobre tus límites de ejecu
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ Todas las respuestas de la API incluyen información sobre tus límites de ejecu
}
```
**Nota:** Los límites de tasa en el cuerpo de la respuesta son para ejecuciones de flujos de trabajo. Los límites de tasa para llamar a este endpoint de la API están en los encabezados de respuesta (`X-RateLimit-*`).
**Nota:** Los límites de tasa utilizan un algoritmo de cubo de tokens. `remaining` puede exceder `requestsPerMinute` hasta `maxBurst` cuando no has usado tu asignación completa recientemente, permitiendo tráfico en ráfagas. Los límites de tasa en el cuerpo de la respuesta son para ejecuciones de flujo de trabajo. Los límites de tasa para llamar a este punto final de la API están en los encabezados de respuesta (`X-RateLimit-*`).
### Consultar registros
@@ -110,13 +112,15 @@ Consulta los registros de ejecución de flujos de trabajo con amplias opciones d
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -190,13 +194,15 @@ Recupera información detallada sobre una entrada de registro específica.
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -482,19 +488,27 @@ Las entregas de webhook fallidas se reintentan con retroceso exponencial y fluct
## Limitación de tasa
La API implementa limitación de tasa para asegurar un uso justo:
La API utiliza un **algoritmo de cubo de tokens** para limitar la tasa, proporcionando un uso justo mientras permite tráfico en ráfagas:
- **Plan gratuito**: 10 solicitudes por minuto
- **Plan Pro**: 30 solicitudes por minuto
- **Plan Team**: 60 solicitudes por minuto
- **Plan Enterprise**: Límites personalizados
| Plan | Solicitudes/Minuto | Capacidad de ráfaga |
|------|-----------------|----------------|
| Free | 10 | 20 |
| Pro | 30 | 60 |
| Team | 60 | 120 |
| Enterprise | 120 | 240 |
La información de límite de tasa se incluye en las cabeceras de respuesta:
- `X-RateLimit-Limit`: Solicitudes máximas por ventana
- `X-RateLimit-Remaining`: Solicitudes restantes en la ventana actual
- `X-RateLimit-Reset`: Marca de tiempo ISO cuando la ventana se reinicia
**Cómo funciona:**
- Los tokens se recargan a una tasa de `requestsPerMinute`
- Puedes acumular hasta `maxBurst` tokens cuando estás inactivo
- Cada solicitud consume 1 token
- La capacidad de ráfaga permite manejar picos de tráfico
## Ejemplo: Sondeo de nuevos registros
La información del límite de tasa se incluye en los encabezados de respuesta:
- `X-RateLimit-Limit`: Solicitudes por minuto (tasa de recarga)
- `X-RateLimit-Remaining`: Tokens disponibles actualmente
- `X-RateLimit-Reset`: Marca de tiempo ISO cuando los tokens se recargan nuevamente
## Ejemplo: Sondeo para nuevos registros
```javascript
let cursor = null;

View File

@@ -147,8 +147,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -159,6 +171,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**Campos de límite de tasa:**
- `requestsPerMinute`: Límite de tasa sostenida (los tokens se recargan a esta velocidad)
- `maxBurst`: Máximo de tokens que puedes acumular (capacidad de ráfaga)
- `remaining`: Tokens disponibles actualmente (puede ser hasta `maxBurst`)
**Campos de respuesta:**
- `currentPeriodCost` refleja el uso en el período de facturación actual
- `limit` se deriva de límites individuales (Gratuito/Pro) o límites agrupados de la organización (Equipo/Empresa)
@@ -170,38 +187,38 @@ Los diferentes planes de suscripción tienen diferentes límites de uso:
| Plan | Límite de uso mensual | Límites de tasa (por minuto) |
|------|-------------------|-------------------------|
| **Gratuito** | $10 | 5 sinc, 10 asinc |
| **Pro** | $100 | 10 sinc, 50 asinc |
| **Equipo** | $500 (agrupado) | 50 sinc, 100 asinc |
| **Gratuito** | $10 | 5 sincrónico, 10 asincrónico |
| **Pro** | $100 | 10 sincrónico, 50 asincrónico |
| **Equipo** | $500 (agrupado) | 50 sincrónico, 100 asincrónico |
| **Empresa** | Personalizado | Personalizado |
## Modelo de facturación
Sim utiliza un modelo de facturación de **suscripción base + exceso**:
Sim utiliza un modelo de facturación de **suscripción base + excedente**:
### Cómo funciona
**Plan Pro ($20/mes):**
- La suscripción mensual incluye $20 de uso
- Uso por debajo de $20 → Sin cargos adicionales
- Uso por encima de $20 → Pagas el exceso al final del mes
- Ejemplo: $35 de uso = $20 (suscripción) + $15 (exceso)
- Uso por encima de $20 → Pagas el excedente al final del mes
- Ejemplo: $35 de uso = $20 (suscripción) + $15 (excedente)
**Plan de equipo ($40/usuario/mes):**
**Plan de Equipo ($40/usuario/mes):**
- Uso agrupado entre todos los miembros del equipo
- Exceso calculado del uso total del equipo
- Excedente calculado del uso total del equipo
- El propietario de la organización recibe una sola factura
**Planes empresariales:**
- Precio mensual fijo, sin excesos
**Planes Empresariales:**
- Precio mensual fijo, sin excedentes
- Límites de uso personalizados según el acuerdo
### Facturación por umbral
Cuando el exceso no facturado alcanza los $50, Sim factura automáticamente el monto total no facturado.
Cuando el excedente no facturado alcanza los $50, Sim factura automáticamente el monto total no facturado.
**Ejemplo:**
- Día 10: $70 de exceso → Factura inmediata de $70
- Día 10: $70 de excedente → Factura inmediata de $70
- Día 15: $35 adicionales de uso ($105 en total) → Ya facturado, sin acción
- Día 20: Otros $50 de uso ($155 en total, $85 no facturados) → Factura inmediata de $85
@@ -209,8 +226,8 @@ Esto distribuye los cargos por exceso a lo largo del mes en lugar de una gran fa
## Mejores prácticas para la gestión de costos
1. **Monitorear regularmente**: Revisa tu panel de uso frecuentemente para evitar sorpresas
2. **Establecer presupuestos**: Usa los límites del plan como guías para tu gasto
1. **Monitorear regularmente**: Revisa tu panel de uso con frecuencia para evitar sorpresas
2. **Establecer presupuestos**: Utiliza los límites del plan como guías para tu gasto
3. **Optimizar flujos de trabajo**: Revisa las ejecuciones de alto costo y optimiza los prompts o la selección de modelos
4. **Usar modelos apropiados**: Ajusta la complejidad del modelo a los requisitos de la tarea
5. **Agrupar tareas similares**: Combina múltiples solicitudes cuando sea posible para reducir la sobrecarga

View File

@@ -49,8 +49,8 @@ Obtener una lista de mercados de predicción de Kalshi con filtrado opcional
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de mercados y metadatos |
| `markets` | array | Array de objetos de mercado |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_market`
@@ -66,8 +66,7 @@ Obtener detalles de un mercado de predicción específico por ticker
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del mercado y metadatos |
| `market` | object | Objeto de mercado con detalles |
### `kalshi_get_events`
@@ -87,8 +86,8 @@ Obtener una lista de eventos de Kalshi con filtrado opcional
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de eventos |
| `events` | array | Array de objetos de eventos |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_event`
@@ -105,8 +104,7 @@ Recuperar detalles de un evento específico por ticker
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos del evento |
| `event` | object | Objeto de evento con detalles |
### `kalshi_get_balance`
@@ -123,8 +121,10 @@ Recuperar el saldo de tu cuenta y el valor de la cartera desde Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos del saldo |
| `balance` | number | Saldo de la cuenta en centavos |
| `portfolioValue` | number | Valor de la cartera en centavos |
| `balanceDollars` | number | Saldo de la cuenta en dólares |
| `portfolioValueDollars` | number | Valor de la cartera en dólares |
### `kalshi_get_positions`
@@ -146,8 +146,8 @@ Recuperar tus posiciones abiertas desde Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de posiciones y metadatos |
| `positions` | array | Array de objetos de posición |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_orders`
@@ -169,8 +169,8 @@ Recupera tus órdenes de Kalshi con filtrado opcional
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de órdenes y metadatos |
| `orders` | array | Array de objetos de órdenes |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_order`
@@ -188,8 +188,7 @@ Recupera detalles de una orden específica por ID desde Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la orden |
| `order` | object | Objeto de orden con detalles |
### `kalshi_get_orderbook`
@@ -205,8 +204,7 @@ Recupera el libro de órdenes (ofertas de sí y no) para un mercado específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del libro de órdenes y metadatos |
| `orderbook` | object | Libro de órdenes con ofertas y demandas de sí/no |
### `kalshi_get_trades`
@@ -223,8 +221,8 @@ Recupera operaciones recientes de todos los mercados
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de operaciones y metadatos |
| `trades` | array | Array de objetos de operaciones |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_candlesticks`
@@ -244,8 +242,7 @@ Obtener datos de velas OHLC para un mercado específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de velas y metadatos |
| `candlesticks` | array | Array de datos de velas OHLC |
### `kalshi_get_fills`
@@ -268,8 +265,8 @@ Recuperar tu portafolio
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de ejecuciones y metadatos |
| `fills` | array | Array de objetos de ejecuciones/operaciones |
| `paging` | object | Cursor de paginación para obtener más resultados |
### `kalshi_get_series_by_ticker`
@@ -285,8 +282,7 @@ Obtener detalles de una serie de mercado específica por ticker
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la serie y metadatos |
| `series` | object | Objeto de serie con detalles |
### `kalshi_get_exchange_status`
@@ -301,8 +297,7 @@ Obtener el estado actual del intercambio Kalshi (actividad de trading y del inte
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del estado del intercambio y metadatos |
| `status` | object | Estado del exchange con indicadores trading_active y exchange_active |
### `kalshi_create_order`
@@ -336,8 +331,7 @@ Crear una nueva orden en un mercado de predicción de Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la orden creada |
| `order` | object | El objeto de orden creada |
### `kalshi_cancel_order`
@@ -355,8 +349,8 @@ Cancelar una orden existente en Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la orden cancelada |
| `order` | object | El objeto de orden cancelada |
| `reducedBy` | number | Número de contratos cancelados |
### `kalshi_amend_order`
@@ -384,8 +378,7 @@ Modificar el precio o la cantidad de una orden existente en Kalshi
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la orden modificada |
| `order` | object | El objeto de orden modificada |
## Notas

View File

@@ -38,21 +38,20 @@ Obtener una lista de mercados de predicción de Polymarket con filtrado opcional
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `closed` | string | No | Filtrar por estado cerrado \(true/false\). Use false para mostrar solo mercados activos. |
| `order` | string | No | Campo de ordenación \(p. ej., volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | No | Dirección de ordenación \(true para ascendente, false para descendente\) |
| `tagId` | string | No | Filtrar por ID de etiqueta |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `limit` | string | No | Número de resultados por página \(máximo 50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de mercados y metadatos |
| `markets` | array | Array de objetos de mercado |
### `polymarket_get_market`
@@ -69,8 +68,7 @@ Obtener detalles de un mercado de predicción específico por ID o slug
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de mercado y metadatos |
| `market` | object | Objeto de mercado con detalles |
### `polymarket_get_events`
@@ -78,21 +76,20 @@ Obtener una lista de eventos de Polymarket con filtrado opcional
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `closed` | string | No | Filtrar por estado cerrado \(true/false\). Use false para mostrar solo eventos activos. |
| `order` | string | No | Campo de ordenación \(p. ej., volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | No | Dirección de ordenación \(true para ascendente, false para descendente\) |
| `tagId` | string | No | Filtrar por ID de etiqueta |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `limit` | string | No | Número de resultados por página \(máximo 50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de eventos y metadatos |
| `events` | array | Array de objetos de eventos |
### `polymarket_get_event`
@@ -109,8 +106,7 @@ Obtener detalles de un evento específico por ID o slug
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos del evento |
| `event` | object | Objeto de evento con detalles |
### `polymarket_get_tags`
@@ -120,15 +116,14 @@ Obtener etiquetas disponibles para filtrar mercados de Polymarket
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `limit` | string | No | Número de resultados por página \(máx 50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de etiquetas |
| `tags` | array | Array de objetos de etiquetas con id, etiqueta y slug |
### `polymarket_search`
@@ -139,15 +134,14 @@ Buscar mercados, eventos y perfiles en Polymarket
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `query` | string | Sí | Término de búsqueda |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
| `limit` | string | No | Número de resultados por página \(máx 50\) |
| `offset` | string | No | Desplazamiento de paginación |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados de búsqueda y metadatos |
| `results` | object | Resultados de búsqueda que contienen arrays de mercados, eventos y perfiles |
### `polymarket_get_series`
@@ -157,7 +151,7 @@ Obtener series (grupos de mercados relacionados) de Polymarket
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `limit` | string | No | Número de resultados por página \(máx 50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
#### Salida
@@ -165,7 +159,7 @@ Obtener series (grupos de mercados relacionados) de Polymarket
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de la serie |
| `output` | object | Datos de series y metadatos |
### `polymarket_get_series_by_id`
@@ -182,7 +176,7 @@ Recuperar una serie específica (grupo de mercado relacionado) por ID desde Poly
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de la serie |
| `output` | object | Datos de serie y metadatos |
### `polymarket_get_orderbook`
@@ -199,7 +193,7 @@ Recuperar el resumen del libro de órdenes para un token específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos del libro de órdenes |
| `output` | object | Datos del libro de órdenes y metadatos |
### `polymarket_get_price`
@@ -217,7 +211,7 @@ Recuperar el precio de mercado para un token y lado específicos
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de precio y metadatos |
| `output` | object | Datos de precio de mercado y metadatos |
### `polymarket_get_midpoint`
@@ -234,7 +228,7 @@ Recuperar el precio medio para un token específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del precio medio y metadatos |
| `output` | object | Datos de precio medio y metadatos |
### `polymarket_get_price_history`
@@ -272,7 +266,7 @@ Recuperar el último precio de operación para un token específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Último precio de operación y metadatos |
| `output` | object | Datos del último precio de operación y metadatos |
### `polymarket_get_spread`
@@ -289,7 +283,7 @@ Recuperar el diferencial de oferta y demanda para un token específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del diferencial y metadatos |
| `output` | object | Datos del diferencial de compra-venta y metadatos |
### `polymarket_get_tick_size`
@@ -306,7 +300,7 @@ Recuperar el tamaño mínimo de tick para un token específico
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Tamaño del tick y metadatos |
| `output` | object | Datos del tamaño mínimo de tick y metadatos |
### `polymarket_get_positions`
@@ -332,19 +326,18 @@ Recuperar historial de operaciones de Polymarket
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `user` | string | No | Dirección de la cartera del usuario para filtrar operaciones |
| `market` | string | No | ID de mercado para filtrar operaciones |
| `limit` | string | No | Número de resultados por página \(recomendado: 25-50\) |
| `limit` | string | No | Número de resultados por página \(máximo 50\) |
| `offset` | string | No | Desplazamiento de paginación \(omitir esta cantidad de resultados\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de operaciones y metadatos |
| `trades` | array | Array de objetos de operaciones |
## Notas

View File

@@ -343,19 +343,30 @@ Eliminar una plantilla de correo electrónico de SendGrid
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `message` | string | Mensaje de estado o éxito |
| `messageId` | string | ID del mensaje de correo electrónico \(send_mail\) |
| `to` | string | Dirección de correo electrónico del destinatario \(send_mail\) |
| `subject` | string | Asunto del correo electrónico \(send_mail, create_template_version\) |
| `id` | string | ID del recurso |
| `jobId` | string | ID del trabajo para operaciones asíncronas |
| `email` | string | Dirección de correo electrónico |
| `firstName` | string | Nombre |
| `lastName` | string | Apellido |
| `jobId` | string | ID de trabajo para operaciones asíncronas |
| `email` | string | Dirección de correo electrónico del contacto |
| `firstName` | string | Nombre del contacto |
| `lastName` | string | Apellido del contacto |
| `createdAt` | string | Marca de tiempo de creación |
| `updatedAt` | string | Marca de tiempo de última actualización |
| `listIds` | json | Array de IDs de listas a las que pertenece el contacto |
| `customFields` | json | Valores de campos personalizados |
| `contacts` | json | Array de contactos |
| `contactCount` | number | Número de contactos |
| `lists` | json | Array de listas |
| `templates` | json | Array de plantillas |
| `message` | string | Estado o mensaje de éxito |
| `name` | string | Nombre del recurso |
| `templates` | json | Array de plantillas |
| `generation` | string | Generación de plantilla |
| `versions` | json | Array de versiones de plantilla |
| `templateId` | string | ID de plantilla |
| `active` | boolean | Si la versión de la plantilla está activa |
| `htmlContent` | string | Contenido HTML |
| `plainContent` | string | Contenido de texto plano |
### `sendgrid_create_template_version`

View File

@@ -0,0 +1,184 @@
---
title: SFTP
description: Transferir archivos a través de SFTP (Protocolo de transferencia de
archivos SSH)
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="sftp"
color="#2D3748"
/>
{/* MANUAL-CONTENT-START:intro */}
[SFTP (Protocolo de transferencia de archivos SSH)](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) es un protocolo de red seguro que te permite subir, descargar y gestionar archivos en servidores remotos. SFTP opera sobre SSH, lo que lo hace ideal para transferencias de archivos automatizadas y cifradas, así como para la gestión remota de archivos dentro de flujos de trabajo modernos.
Con las herramientas SFTP integradas en Sim, puedes automatizar fácilmente el movimiento de archivos entre tus agentes de IA y sistemas o servidores externos. Esto permite a tus agentes gestionar intercambios críticos de datos, copias de seguridad, generación de documentos y orquestación de sistemas remotos, todo con una seguridad robusta.
**Funcionalidades clave disponibles a través de las herramientas SFTP:**
- **Subir archivos:** Transfiere sin problemas archivos de cualquier tipo desde tu flujo de trabajo a un servidor remoto, con soporte tanto para autenticación por contraseña como por clave privada SSH.
- **Descargar archivos:** Recupera archivos de servidores SFTP remotos directamente para su procesamiento, archivo o automatización adicional.
- **Listar y gestionar archivos:** Enumera directorios, elimina o crea archivos y carpetas, y gestiona permisos del sistema de archivos de forma remota.
- **Autenticación flexible:** Conéctate usando contraseñas tradicionales o claves SSH, con soporte para frases de contraseña y control de permisos.
- **Soporte para archivos grandes:** Gestiona programáticamente cargas y descargas de archivos grandes, con límites de tamaño incorporados para mayor seguridad.
Al integrar SFTP en Sim, puedes automatizar operaciones seguras de archivos como parte de cualquier flujo de trabajo, ya sea recopilación de datos, informes, mantenimiento de sistemas remotos o intercambio dinámico de contenido entre plataformas.
Las secciones a continuación describen las principales herramientas SFTP disponibles:
- **sftp_upload:** Sube uno o más archivos a un servidor remoto.
- **sftp_download:** Descarga archivos desde un servidor remoto a tu flujo de trabajo.
- **sftp_list:** Lista el contenido de directorios en un servidor SFTP remoto.
- **sftp_delete:** Elimina archivos o directorios de un servidor remoto.
- **sftp_create:** Crea nuevos archivos en un servidor SFTP remoto.
- **sftp_mkdir:** Crea nuevos directorios de forma remota.
Consulta la documentación de la herramienta a continuación para conocer los parámetros detallados de entrada y salida para cada operación.
{/* MANUAL-CONTENT-END */}
## Instrucciones de uso
Sube, descarga, lista y gestiona archivos en servidores remotos a través de SFTP. Compatible con autenticación por contraseña y clave privada para transferencias seguras de archivos.
## Herramientas
### `sftp_upload`
Subir archivos a un servidor SFTP remoto
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `host` | string | Sí | Nombre de host o dirección IP del servidor SFTP |
| `port` | number | Sí | Puerto del servidor SFTP \(predeterminado: 22\) |
| `username` | string | Sí | Nombre de usuario SFTP |
| `password` | string | No | Contraseña para autenticación \(si no se usa clave privada\) |
| `privateKey` | string | No | Clave privada para autenticación \(formato OpenSSH\) |
| `passphrase` | string | No | Frase de contraseña para clave privada cifrada |
| `remotePath` | string | Sí | Directorio de destino en el servidor remoto |
| `files` | file[] | No | Archivos para subir |
| `fileContent` | string | No | Contenido directo del archivo para subir \(para archivos de texto\) |
| `fileName` | string | No | Nombre del archivo cuando se usa contenido directo |
| `overwrite` | boolean | No | Si se deben sobrescribir archivos existentes \(predeterminado: true\) |
| `permissions` | string | No | Permisos del archivo \(p. ej., 0644\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la subida fue exitosa |
| `uploadedFiles` | json | Array de detalles de archivos subidos \(nombre, rutaRemota, tamaño\) |
| `message` | string | Mensaje de estado de la operación |
### `sftp_download`
Descargar un archivo desde un servidor SFTP remoto
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `host` | string | Sí | Nombre de host o dirección IP del servidor SFTP |
| `port` | number | Sí | Puerto del servidor SFTP \(predeterminado: 22\) |
| `username` | string | Sí | Nombre de usuario SFTP |
| `password` | string | No | Contraseña para autenticación \(si no se usa clave privada\) |
| `privateKey` | string | No | Clave privada para autenticación \(formato OpenSSH\) |
| `passphrase` | string | No | Frase de contraseña para clave privada cifrada |
| `remotePath` | string | Sí | Ruta al archivo en el servidor remoto |
| `encoding` | string | No | Codificación de salida: utf-8 para texto, base64 para binario \(predeterminado: utf-8\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la descarga fue exitosa |
| `fileName` | string | Nombre del archivo descargado |
| `content` | string | Contenido del archivo \(texto o codificado en base64\) |
| `size` | number | Tamaño del archivo en bytes |
| `encoding` | string | Codificación del contenido \(utf-8 o base64\) |
| `message` | string | Mensaje de estado de la operación |
### `sftp_list`
Listar archivos y directorios en un servidor SFTP remoto
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `host` | string | Sí | Nombre de host o dirección IP del servidor SFTP |
| `port` | number | Sí | Puerto del servidor SFTP \(predeterminado: 22\) |
| `username` | string | Sí | Nombre de usuario SFTP |
| `password` | string | No | Contraseña para autenticación \(si no se usa clave privada\) |
| `privateKey` | string | No | Clave privada para autenticación \(formato OpenSSH\) |
| `passphrase` | string | No | Frase de contraseña para clave privada cifrada |
| `remotePath` | string | Sí | Ruta del directorio en el servidor remoto |
| `detailed` | boolean | No | Incluir información detallada de archivos \(tamaño, permisos, fecha de modificación\) |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la operación fue exitosa |
| `path` | string | Ruta del directorio que fue listado |
| `entries` | json | Array de entradas del directorio con nombre, tipo, tamaño, permisos, modifiedAt |
| `count` | number | Número de entradas en el directorio |
| `message` | string | Mensaje de estado de la operación |
### `sftp_delete`
Eliminar un archivo o directorio en un servidor SFTP remoto
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
| `host` | string | Sí | Nombre de host o dirección IP del servidor SFTP |
| `port` | number | Sí | Puerto del servidor SFTP \(predeterminado: 22\) |
| `username` | string | Sí | Nombre de usuario SFTP |
| `password` | string | No | Contraseña para autenticación \(si no se usa clave privada\) |
| `privateKey` | string | No | Clave privada para autenticación \(formato OpenSSH\) |
| `passphrase` | string | No | Frase de contraseña para clave privada cifrada |
| `remotePath` | string | Sí | Ruta al archivo o directorio a eliminar |
| `recursive` | boolean | No | Eliminar directorios recursivamente |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si la eliminación fue exitosa |
| `deletedPath` | string | Ruta que fue eliminada |
| `message` | string | Mensaje de estado de la operación |
### `sftp_mkdir`
Crear un directorio en un servidor SFTP remoto
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `host` | string | Sí | Nombre de host o dirección IP del servidor SFTP |
| `port` | number | Sí | Puerto del servidor SFTP \(predeterminado: 22\) |
| `username` | string | Sí | Nombre de usuario SFTP |
| `password` | string | No | Contraseña para autenticación \(si no se usa clave privada\) |
| `privateKey` | string | No | Clave privada para autenticación \(formato OpenSSH\) |
| `passphrase` | string | No | Frase de contraseña para clave privada cifrada |
| `remotePath` | string | Sí | Ruta para el nuevo directorio |
| `recursive` | boolean | No | Crear directorios principales si no existen |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Si el directorio se creó correctamente |
| `createdPath` | string | Ruta del directorio creado |
| `message` | string | Mensaje de estado de la operación |
## Notas
- Categoría: `tools`
- Tipo: `sftp`

View File

@@ -30,6 +30,9 @@ Utiliza el bloque Start para todo lo que se origina desde el editor, despliegue
<Card title="Schedule" href="/triggers/schedule">
Ejecución basada en cron o intervalos
</Card>
<Card title="RSS Feed" href="/triggers/rss">
Monitorea feeds RSS y Atom para nuevo contenido
</Card>
</Cards>
## Comparación rápida
@@ -39,6 +42,7 @@ Utiliza el bloque Start para todo lo que se origina desde el editor, despliegue
| **Start** | Ejecuciones del editor, solicitudes de despliegue a API o mensajes de chat |
| **Schedule** | Temporizador gestionado en el bloque de programación |
| **Webhook** | Al recibir una solicitud HTTP entrante |
| **RSS Feed** | Nuevo elemento publicado en el feed |
> El bloque Start siempre expone los campos `input`, `conversationId` y `files`. Añade campos personalizados al formato de entrada para datos estructurados adicionales.

View File

@@ -0,0 +1,49 @@
---
title: Feed RSS
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
El bloque de Feed RSS monitorea feeds RSS y Atom cuando se publican nuevos elementos, tu flujo de trabajo se activa automáticamente.
<div className="flex justify-center">
<Image
src="/static/blocks/rss.png"
alt="Bloque de Feed RSS"
width={500}
height={400}
className="my-6"
/>
</div>
## Configuración
1. **Añadir bloque de Feed RSS** - Arrastra el bloque de Feed RSS para iniciar tu flujo de trabajo
2. **Introducir URL del feed** - Pega la URL de cualquier feed RSS o Atom
3. **Implementar** - Implementa tu flujo de trabajo para activar el sondeo
Una vez implementado, el feed se comprueba cada minuto en busca de nuevos elementos.
## Campos de salida
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `title` | string | Título del elemento |
| `link` | string | Enlace del elemento |
| `pubDate` | string | Fecha de publicación |
| `item` | object | Elemento en bruto con todos los campos |
| `feed` | object | Metadatos en bruto del feed |
Accede a los campos mapeados directamente (`<rss.title>`) o utiliza los objetos en bruto para cualquier campo (`<rss.item.author>`, `<rss.feed.language>`).
## Casos de uso
- **Monitoreo de contenido** - Sigue blogs, sitios de noticias o actualizaciones de competidores
- **Automatización de podcasts** - Activa flujos de trabajo cuando se publican nuevos episodios
- **Seguimiento de lanzamientos** - Monitorea lanzamientos de GitHub, registros de cambios o actualizaciones de productos
- **Agregación social** - Recopila contenido de plataformas que exponen feeds RSS
<Callout>
Los disparadores RSS solo se activan para elementos publicados después de guardar el disparador. Los elementos existentes en el feed no se procesan.
</Callout>

View File

@@ -27,14 +27,16 @@ Toutes les réponses API incluent des informations sur vos limites d'exécution
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ Toutes les réponses API incluent des informations sur vos limites d'exécution
}
```
**Remarque :** Les limites de débit dans le corps de la réponse concernent les exécutions de workflow. Les limites de débit pour l'appel de ce point de terminaison API se trouvent dans les en-têtes de réponse (`X-RateLimit-*`).
**Remarque :** les limites de débit utilisent un algorithme de seau à jetons. `remaining` peut dépasser `requestsPerMinute` jusqu'à `maxBurst` lorsque vous n'avez pas utilisé récemment votre allocation complète, permettant ainsi un trafic en rafale. Les limites de débit dans le corps de la réponse concernent les exécutions de workflow. Les limites de débit pour appeler ce point de terminaison API se trouvent dans les en-têtes de réponse (`X-RateLimit-*`).
### Interrogation des journaux
@@ -110,13 +112,15 @@ Interrogez les journaux d'exécution des workflows avec de nombreuses options de
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -190,13 +194,15 @@ Récupérer des informations détaillées sur une entrée de journal spécifique
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -482,19 +488,27 @@ Les livraisons de webhook échouées sont réessayées avec un backoff exponenti
## Limitation de débit
L'API implémente une limitation de débit pour garantir une utilisation équitable :
L'API utilise un **algorithme de seau à jetons** pour limiter le débit, offrant une utilisation équitable tout en permettant des pics de trafic :
- **Plan gratuit** : 10 requêtes par minute
- **Plan Pro** : 30 requêtes par minute
- **Plan Équipe** : 60 requêtes par minute
- **Plan Entreprise** : Limites personnalisées
| Forfait | Requêtes/minute | Capacité de rafale |
|------|-----------------|----------------|
| Gratuit | 10 | 20 |
| Pro | 30 | 60 |
| Équipe | 60 | 120 |
| Entreprise | 120 | 240 |
Les informations de limitation de débit sont incluses dans les en-têtes de réponse :
- `X-RateLimit-Limit` : Nombre maximum de requêtes par fenêtre
- `X-RateLimit-Remaining` : Requêtes restantes dans la fenêtre actuelle
- `X-RateLimit-Reset` : Horodatage ISO indiquant quand la fenêtre se réinitialise
**Comment ça fonctionne :**
- Les jetons se rechargent au rythme de `requestsPerMinute`
- Vous pouvez accumuler jusqu'à `maxBurst` jetons en période d'inactivité
- Chaque requête consomme 1 jeton
- La capacité de rafale permet de gérer les pics de trafic
## Exemple : Polling pour nouveaux logs
Les informations sur les limites de débit sont incluses dans les en-têtes de réponse :
- `X-RateLimit-Limit` : requêtes par minute (taux de recharge)
- `X-RateLimit-Remaining` : jetons actuellement disponibles
- `X-RateLimit-Reset` : horodatage ISO indiquant quand les jetons seront rechargés
## Exemple : interrogation pour de nouveaux journaux
```javascript
let cursor = null;
@@ -541,7 +555,7 @@ async function pollLogs() {
setInterval(pollLogs, 30000);
```
## Exemple : Traitement des webhooks
## Exemple : traitement des webhooks
```javascript
import express from 'express';

View File

@@ -147,8 +147,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -159,6 +171,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**Champs de limite de débit :**
- `requestsPerMinute` : limite de débit soutenu (les jetons se rechargent à ce rythme)
- `maxBurst` : nombre maximum de jetons que vous pouvez accumuler (capacité de rafale)
- `remaining` : jetons actuellement disponibles (peut aller jusqu'à `maxBurst`)
**Champs de réponse :**
- `currentPeriodCost` reflète l'utilisation dans la période de facturation actuelle
- `limit` est dérivé des limites individuelles (Gratuit/Pro) ou des limites mutualisées de l'organisation (Équipe/Entreprise)
@@ -188,7 +205,7 @@ Sim utilise un modèle de facturation **abonnement de base + dépassement** :
- Exemple : 35 $ d'utilisation = 20 $ (abonnement) + 15 $ (dépassement)
**Forfait Équipe (40 $/siège/mois) :**
- Utilisation mutualisée entre tous les membres de l'équipe
- Utilisation mutualisée pour tous les membres de l'équipe
- Dépassement calculé à partir de l'utilisation totale de l'équipe
- Le propriétaire de l'organisation reçoit une seule facture

View File

@@ -49,8 +49,8 @@ Récupérer une liste de marchés prédictifs de Kalshi avec filtrage optionnel
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Données des marchés et métadonnées |
| `markets` | array | Tableau d'objets de marché |
| `paging` | object | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_market`
@@ -66,8 +66,7 @@ Récupérer les détails d'un marché prédictif spécifique par code
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Données du marché et métadonnées |
| `market` | object | Objet de marché avec détails |
### `kalshi_get_events`
@@ -87,8 +86,8 @@ Récupérer une liste d'événements de Kalshi avec filtrage optionnel
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées des événements |
| `events` | array | Tableau d'objets d'événement |
| `paging` | object | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_event`
@@ -105,8 +104,7 @@ Récupérer les détails d'un événement spécifique par ticker
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées de l'événement |
| `event` | object | Objet d'événement avec détails |
### `kalshi_get_balance`
@@ -123,8 +121,10 @@ Récupérer le solde de votre compte et la valeur de votre portefeuille depuis K
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées du solde |
| `balance` | number | Solde du compte en centimes |
| `portfolioValue` | number | Valeur du portefeuille en centimes |
| `balanceDollars` | number | Solde du compte en dollars |
| `portfolioValueDollars` | number | Valeur du portefeuille en dollars |
### `kalshi_get_positions`
@@ -146,8 +146,8 @@ Récupérer vos positions ouvertes depuis Kalshi
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de positions et métadonnées |
| `positions` | array | Tableau d'objets de position |
| `paging` | object | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_orders`
@@ -169,8 +169,8 @@ Récupérez vos ordres depuis Kalshi avec filtrage optionnel
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données des ordres et métadonnées |
| `orders` | array | Tableau d'objets d'ordre |
| `paging` | object | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_order`
@@ -188,8 +188,7 @@ Récupérer les détails d'un ordre spécifique par ID depuis Kalshi
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'ordre |
| `order` | object | Objet d'ordre avec détails |
### `kalshi_get_orderbook`
@@ -205,8 +204,7 @@ Récupérer le carnet d'ordres (offres oui et non) pour un marché spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données du carnet d'ordres et métadonnées |
| `orderbook` | objet | Carnet d'ordres avec offres et demandes oui/non |
### `kalshi_get_trades`
@@ -223,8 +221,8 @@ Récupérer les transactions récentes sur tous les marchés
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données des transactions et métadonnées |
| `trades` | tableau | Tableau d'objets de transactions |
| `paging` | objet | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_candlesticks`
@@ -244,8 +242,7 @@ Récupérer les données de chandeliers OHLC pour un marché spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de chandeliers et métadonnées |
| `candlesticks` | tableau | Tableau de données de chandeliers OHLC |
### `kalshi_get_fills`
@@ -268,8 +265,8 @@ Récupérer votre portefeuille
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées des exécutions |
| `fills` | tableau | Tableau d'objets d'exécutions/transactions |
| `paging` | objet | Curseur de pagination pour récupérer plus de résultats |
### `kalshi_get_series_by_ticker`
@@ -285,8 +282,7 @@ Récupérer les détails d'une série de marché spécifique par ticker
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées de la série |
| `series` | objet | Objet de série avec détails |
### `kalshi_get_exchange_status`
@@ -301,8 +297,7 @@ Récupérer le statut actuel de la plateforme d'échange Kalshi (activité de tr
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées du statut de l'échange |
| `status` | objet | Statut de l'échange avec indicateurs trading_active et exchange_active |
### `kalshi_create_order`
@@ -336,8 +331,7 @@ Créer un nouvel ordre sur un marché de prédiction Kalshi
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'ordre créé |
| `order` | objet | L'objet de l'ordre créé |
### `kalshi_cancel_order`
@@ -355,8 +349,8 @@ Annuler un ordre existant sur Kalshi
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'ordre annulé |
| `order` | objet | L'objet de l'ordre annulé |
| `reducedBy` | nombre | Nombre de contrats annulés |
### `kalshi_amend_order`
@@ -384,8 +378,7 @@ Modifier le prix ou la quantité d'un ordre existant sur Kalshi
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'ordre modifié |
| `order` | objet | L'objet de l'ordre modifié |
## Notes

View File

@@ -40,19 +40,18 @@ Récupérer une liste des marchés prédictifs de Polymarket avec filtrage optio
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `closed` | string | Non | Filtrer par statut fermé \(true/false\). Utilisez false pour les marchés actifs uniquement. |
| `order` | string | Non | Champ de tri \(par exemple, volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `closed` | string | Non | Filtrer par statut fermé \(true/false\). Utiliser false pour les marchés actifs uniquement. |
| `order` | string | Non | Champ de tri \(ex. volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | Non | Direction de tri \(true pour ascendant, false pour descendant\) |
| `tagId` | string | Non | Filtrer par ID de tag |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Données des marchés et métadonnées |
| `markets` | array | Tableau d'objets de marché |
### `polymarket_get_market`
@@ -69,8 +68,7 @@ Récupérer les détails d'un marché prédictif spécifique par ID ou slug
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de marché et métadonnées |
| `market` | object | Objet de marché avec détails |
### `polymarket_get_events`
@@ -80,19 +78,18 @@ Récupérer une liste d'événements de Polymarket avec filtrage optionnel
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `closed` | string | Non | Filtrer par statut fermé \(true/false\). Utilisez false pour les événements actifs uniquement. |
| `order` | string | Non | Champ de tri \(par exemple, volume, liquidity, startDate, endDate, createdAt\) |
| `closed` | string | Non | Filtrer par statut fermé \(true/false\). Utiliser false pour les événements actifs uniquement. |
| `order` | string | Non | Champ de tri \(ex. volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | Non | Direction de tri \(true pour ascendant, false pour descendant\) |
| `tagId` | string | Non | Filtrer par ID de tag |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données d'événements et métadonnées |
| `events` | array | Tableau d'objets d'événements |
### `polymarket_get_event`
@@ -109,8 +106,7 @@ Récupérer les détails d'un événement spécifique par ID ou slug
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées de l'événement |
| `event` | object | Objet d'événement avec détails |
### `polymarket_get_tags`
@@ -119,16 +115,15 @@ Récupérer les tags disponibles pour filtrer les marchés sur Polymarket
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| --------- | ---- | ---------- | ----------- |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées des tags |
| `tags` | array | Tableau d'objets de tags avec id, label et slug |
### `polymarket_search`
@@ -137,17 +132,16 @@ Rechercher des marchés, des événements et des profils sur Polymarket
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `query` | string | Oui | Terme de recherche |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultats de recherche et métadonnées |
| `results` | object | Résultats de recherche contenant des tableaux de marchés, d'événements et de profils |
### `polymarket_get_series`
@@ -156,8 +150,8 @@ Récupérer des séries (groupes de marchés liés) depuis Polymarket
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| --------- | ---- | ---------- | ----------- |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
#### Sortie
@@ -165,7 +159,7 @@ Récupérer des séries (groupes de marchés liés) depuis Polymarket
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées de la série |
| `output` | object | Données des séries et métadonnées |
### `polymarket_get_series_by_id`
@@ -182,7 +176,7 @@ Récupérer une série spécifique (groupe de marché associé) par ID depuis Po
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées de la série |
| `output` | object | Données de série et métadonnées |
### `polymarket_get_orderbook`
@@ -199,7 +193,7 @@ Récupérer le résumé du carnet d'ordres pour un jeton spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées du carnet d'ordres |
| `output` | object | Données du carnet d'ordres et métadonnées |
### `polymarket_get_price`
@@ -217,7 +211,7 @@ Récupérer le prix du marché pour un jeton et un côté spécifiques
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de prix et métadonnées |
| `output` | object | Données de prix du marché et métadonnées |
### `polymarket_get_midpoint`
@@ -255,7 +249,7 @@ Récupérer les données historiques de prix pour un jeton de marché spécifiqu
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données d'historique de prix et métadonnées |
| `output` | object | Données d'historique des prix et métadonnées |
### `polymarket_get_last_trade_price`
@@ -272,7 +266,7 @@ Récupérer le dernier prix de transaction pour un jeton spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Dernier prix de transaction et métadonnées |
| `output` | object | Données du dernier prix d'échange et métadonnées |
### `polymarket_get_spread`
@@ -289,7 +283,7 @@ Récupérer l'écart entre l'offre et la demande pour un jeton spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données d'écart et métadonnées |
| `output` | object | Données d'écart achat-vente et métadonnées |
### `polymarket_get_tick_size`
@@ -306,7 +300,7 @@ Récupérer la taille minimale du tick pour un jeton spécifique
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Taille du tick et métadonnées |
| `output` | object | Données de taille minimale de tick et métadonnées |
### `polymarket_get_positions`
@@ -336,15 +330,14 @@ Récupérer l'historique des transactions depuis Polymarket
| --------- | ---- | ---------- | ----------- |
| `user` | string | Non | Adresse du portefeuille de l'utilisateur pour filtrer les transactions |
| `market` | string | Non | ID de marché pour filtrer les transactions |
| `limit` | string | Non | Nombre de résultats par page \(recommandé : 25-50\) |
| `limit` | string | Non | Nombre de résultats par page \(max 50\) |
| `offset` | string | Non | Décalage de pagination \(ignorer ce nombre de résultats\) |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données des transactions et métadonnées |
| `trades` | array | Tableau d'objets de transactions |
## Notes

View File

@@ -342,20 +342,31 @@ Supprimer un modèle d'e-mail de SendGrid
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `messageId` | chaîne | ID du message e-mail \(send_mail\) |
| `id` | chaîne | ID de la ressource |
| `jobId` | chaîne | ID de tâche pour les opérations asynchrones |
| `email` | chaîne | Adresse e-mail |
| `firstName` | chaîne | Prénom |
| `lastName` | chaîne | Nom de famille |
| `success` | boolean | Statut de réussite de l'opération |
| `message` | string | Message d'état ou de réussite |
| `messageId` | string | ID du message e-mail \(send_mail\) |
| `to` | string | Adresse e-mail du destinataire \(send_mail\) |
| `subject` | string | Objet de l'e-mail \(send_mail, create_template_version\) |
| `id` | string | ID de la ressource |
| `jobId` | string | ID de tâche pour les opérations asynchrones |
| `email` | string | Adresse e-mail du contact |
| `firstName` | string | Prénom du contact |
| `lastName` | string | Nom de famille du contact |
| `createdAt` | string | Horodatage de création |
| `updatedAt` | string | Horodatage de dernière mise à jour |
| `listIds` | json | Tableau des ID de listes auxquelles le contact appartient |
| `customFields` | json | Valeurs des champs personnalisés |
| `contacts` | json | Tableau de contacts |
| `contactCount` | nombre | Nombre de contacts |
| `contactCount` | number | Nombre de contacts |
| `lists` | json | Tableau de listes |
| `name` | string | Nom de la ressource |
| `templates` | json | Tableau de modèles |
| `message` | chaîne | Statut ou message de réussite |
| `name` | chaîne | Nom de la ressource |
| `generation` | chaîne | Génération du modèle |
| `generation` | string | Génération du modèle |
| `versions` | json | Tableau des versions du modèle |
| `templateId` | string | ID du modèle |
| `active` | boolean | Si la version du modèle est active |
| `htmlContent` | string | Contenu HTML |
| `plainContent` | string | Contenu en texte brut |
### `sendgrid_create_template_version`

View File

@@ -21,24 +21,28 @@ import { Image } from '@/components/ui/image'
Utilisez le bloc Démarrer pour tout ce qui provient de l'éditeur, du déploiement vers l'API ou des expériences de déploiement vers le chat. D'autres déclencheurs restent disponibles pour les flux de travail basés sur des événements :
<Cards>
<Card title="Démarrer" href="/triggers/start">
<Card title="Start" href="/triggers/start">
Point d'entrée unifié qui prend en charge les exécutions de l'éditeur, les déploiements d'API et les déploiements de chat
</Card>
<Card title="Webhook" href="/triggers/webhook">
Recevoir des charges utiles de webhook externes
</Card>
<Card title="Planification" href="/triggers/schedule">
<Card title="Schedule" href="/triggers/schedule">
Exécution basée sur cron ou intervalle
</Card>
<Card title="RSS Feed" href="/triggers/rss">
Surveiller les flux RSS et Atom pour du nouveau contenu
</Card>
</Cards>
## Comparaison rapide
| Déclencheur | Condition de démarrage |
|---------|-----------------|
| **Démarrer** | Exécutions de l'éditeur, requêtes de déploiement vers l'API ou messages de chat |
| **Planification** | Minuteur géré dans le bloc de planification |
| **Start** | Exécutions de l'éditeur, requêtes de déploiement d'API ou messages de chat |
| **Schedule** | Minuteur géré dans le bloc de planification |
| **Webhook** | Sur requête HTTP entrante |
| **RSS Feed** | Nouvel élément publié dans le flux |
> Le bloc Démarrer expose toujours les champs `input`, `conversationId` et `files`. Ajoutez des champs personnalisés au format d'entrée pour des données structurées supplémentaires.

View File

@@ -0,0 +1,49 @@
---
title: Flux RSS
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Le bloc Flux RSS surveille les flux RSS et Atom lorsque de nouveaux éléments sont publiés, votre workflow se déclenche automatiquement.
<div className="flex justify-center">
<Image
src="/static/blocks/rss.png"
alt="Bloc Flux RSS"
width={500}
height={400}
className="my-6"
/>
</div>
## Configuration
1. **Ajouter le bloc Flux RSS** - Faites glisser le bloc Flux RSS pour démarrer votre workflow
2. **Saisir l'URL du flux** - Collez l'URL de n'importe quel flux RSS ou Atom
3. **Déployer** - Déployez votre workflow pour activer l'interrogation
Une fois déployé, le flux est vérifié chaque minute pour détecter de nouveaux éléments.
## Champs de sortie
| Champ | Type | Description |
|-------|------|-------------|
| `title` | string | Titre de l'élément |
| `link` | string | Lien de l'élément |
| `pubDate` | string | Date de publication |
| `item` | object | Élément brut avec tous les champs |
| `feed` | object | Métadonnées brutes du flux |
Accédez directement aux champs mappés (`<rss.title>`) ou utilisez les objets bruts pour n'importe quel champ (`<rss.item.author>`, `<rss.feed.language>`).
## Cas d'utilisation
- **Surveillance de contenu** - Suivez les blogs, sites d'actualités ou mises à jour des concurrents
- **Automatisation de podcast** - Déclenchez des workflows lors de la sortie de nouveaux épisodes
- **Suivi des versions** - Surveillez les versions GitHub, les journaux de modifications ou les mises à jour de produits
- **Agrégation sociale** - Collectez du contenu à partir de plateformes qui exposent des flux RSS
<Callout>
Les déclencheurs RSS ne s'activent que pour les éléments publiés après l'enregistrement du déclencheur. Les éléments existants du flux ne sont pas traités.
</Callout>

View File

@@ -27,14 +27,16 @@ SimダッシュボードのユーザーセッティングからAPIキーを生
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ SimダッシュボードのユーザーセッティングからAPIキーを生
}
```
**注意** レスポンス本文のレート制限はワークフロー実行に関するものです。このAPIエンドポイントを呼び出すためのレート制限はレスポンスヘッダー`X-RateLimit-*`)にあります。
**注意:** レート制限はトークンバケットアルゴリズムを使用しています。最近の割り当てを完全に使用していない場合、`remaining`は`requestsPerMinute`を超えて`maxBurst`まで達することができ、バーストトラフィックを許可します。レスポンスボディのレート制限はワークフロー実行のためのものです。このAPIエンドポイントを呼び出すためのレート制限はレスポンスヘッダー`X-RateLimit-*`)にあります。
### ログの照会
@@ -110,13 +112,15 @@ SimダッシュボードのユーザーセッティングからAPIキーを生
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -190,13 +194,15 @@ SimダッシュボードのユーザーセッティングからAPIキーを生
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -482,17 +488,25 @@ Webhookシークレットを設定した場合、署名を検証してWebhookが
## レート制限
APIは公平な使用を確保するためにレート制限を実装しています:
APIは**トークンバケットアルゴリズム**をレート制限に使用し、バーストトラフィックを許可しながら公平な使用を提供します:
- **無料プラン**: 1分あたり10リクエスト
- **プロプラン**: 1分あたり30リクエスト
- **チームプラン**: 1分あたり60リクエスト
- **エンタープライズプラン**: カスタム制限
| プラン | リクエスト/分 | バースト容量 |
|------|-----------------|----------------|
| 無料 | 10 | 20 |
| プロ | 30 | 60 |
| チーム | 60 | 120 |
| エンタープライズ | 120 | 240 |
**仕組み:**
- トークンは`requestsPerMinute`のレートで補充されます
- アイドル状態のとき、最大`maxBurst`トークンまで蓄積できます
- 各リクエストは1トークンを消費します
- バースト容量によりトラフィックスパイクの処理が可能になります
レート制限情報はレスポンスヘッダーに含まれています:
- `X-RateLimit-Limit`: ウィンドウあたりの最大リクエスト数
- `X-RateLimit-Remaining`: 現在のウィンドウで残っているリクエスト数
- `X-RateLimit-Reset`: ウィンドウがリセットされるISOタイムスタンプ
- `X-RateLimit-Limit`1分あたりのリクエスト数(補充レート)
- `X-RateLimit-Remaining`:現在利用可能なトークン
- `X-RateLimit-Reset`:トークンが次に補充されるISOタイムスタンプ
## 例:新しいログのポーリング
@@ -541,7 +555,7 @@ async function pollLogs() {
setInterval(pollLogs, 30000);
```
## 例:Webhookの処理
## 例:ウェブフックの処理
```javascript
import express from 'express';

View File

@@ -147,8 +147,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -159,35 +171,40 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**レート制限フィールド:**
- `requestsPerMinute`:持続的なレート制限(トークンはこの速度で補充されます)
- `maxBurst`:蓄積できる最大トークン数(バースト容量)
- `remaining`:現在利用可能なトークン(最大で`maxBurst`まで)
**レスポンスフィールド:**
- `currentPeriodCost` は現在の請求期間の使用状況を反映します
- `limit` は個別の制限(無料/プロ)または組織のプール制限(チーム/エンタープライズ)から導出されます
- `plan` はユーザーに関連付けられた最優先のアクティブなプランです
- `currentPeriodCost`は現在の請求期間の使用状況を反映します
- `limit`は個別の制限(無料/プロ)または組織のプール制限(チーム/エンタープライズ)から派生します
- `plan`はユーザーに関連付けられた最優先のアクティブなプランです
## プラン制限
異なるサブスクリプションプランには異なる使用制限がります:
サブスクリプションプランによって使用制限が異なります:
| プラン | 月間使用制限 | レート制限(分あたり) |
|------|-------------------|-------------------------|
| **無料** | $10 | 5 同期、10 非同期 |
| **プロ** | $100 | 10 同期、50 非同期 |
| **チーム** | $500プール | 50 同期、100 非同期 |
| **無料** | $10 | 5同期、10非同期 |
| **プロ** | $100 | 10同期、50非同期 |
| **チーム** | $500プール | 50同期、100非同期 |
| **エンタープライズ** | カスタム | カスタム |
## 課金モデル
Simは**基本サブスクリプション + 超過分**の課金モデルを使用しています:
Simは**基本サブスクリプション+超過分**の課金モデルを使用しています:
### 仕組み
**プロプラン(月額$20**
- 月額サブスクリプションには$20分の使用量が含まれます
- 使用量が$20未満 → 追加料金なし
- 使用量が$20を超える → 月末に超過分を支払
- 使用量が$20を超える → 月末に超過分を支払
- 例:$35の使用量 = $20サブスクリプション+ $15超過分
**チームプラン(月額$40/シート**
**チームプラン(席あたり月額$40**
- チームメンバー全体でプールされた使用量
- チーム全体の使用量から超過分を計算
- 組織のオーナーが一括で請求を受ける
@@ -209,10 +226,10 @@ Simは**基本サブスクリプション + 超過分**の課金モデルを使
## コスト管理のベストプラクティス
1. **定期的な監視**: 予想外の事態を避けるため、使用状況ダッシュボードを頻繁に確認する
1. **定期的な監視**: 予期せぬ事態を避けるため、使用状況ダッシュボードを頻繁に確認する
2. **予算の設定**: プランの制限を支出のガードレールとして使用する
3. **ワークフローの最適化**: コストの高い実行を見直し、プロンプトやモデル選択を最適化する
4. **適切なモデルの使用**: タスクの要件に合わせてモデルの複雑さを調整す
4. **適切なモデルの使用**: タスクの要件にモデルの複雑さを合わせ
5. **類似タスクのバッチ処理**: 可能な場合は複数のリクエストを組み合わせてオーバーヘッドを削減する
## 次のステップ

View File

@@ -49,8 +49,8 @@ Kalshiの予測市場をワークフローに統合します。市場一覧、
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 市場データとメタデータ |
| `markets` | array | 市場オブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_market`
@@ -66,8 +66,7 @@ Kalshiの予測市場をワークフローに統合します。市場一覧、
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 市場データとメタデータ |
| `market` | object | 詳細情報を含む市場オブジェクト |
### `kalshi_get_events`
@@ -87,8 +86,8 @@ Kalshiの予測市場をワークフローに統合します。市場一覧、
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | イベントデータとメタデータ |
| `events` | array | イベントオブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_event`
@@ -105,8 +104,7 @@ Kalshiの予測市場をワークフローに統合します。市場一覧、
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | イベントデータとメタデータ |
| `event` | object | 詳細情報を含むイベントオブジェクト |
### `kalshi_get_balance`
@@ -123,8 +121,10 @@ Kalshiからアカウント残高とポートフォリオ価値を取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 残高データとメタデータ |
| `balance` | number | セント単位のアカウント残高 |
| `portfolioValue` | number | セント単位のポートフォリオ価値 |
| `balanceDollars` | number | ドル単位のアカウント残高 |
| `portfolioValueDollars` | number | ドル単位のポートフォリオ価値 |
### `kalshi_get_positions`
@@ -146,8 +146,8 @@ Kalshiからオープンポジションを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ポジションデータとメタデータ |
| `positions` | array | ポジションオブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_orders`
@@ -169,8 +169,8 @@ Kalshiからオープンポジションを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 注文データとメタデータ |
| `orders` | array | 注文オブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_order`
@@ -188,8 +188,7 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 注文データ |
| `order` | object | 詳細情報を含む注文オブジェクト |
### `kalshi_get_orderbook`
@@ -205,8 +204,7 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 注文板データとメタデータ |
| `orderbook` | object | yes/noの買い注文と売り注文を含むオーダーブック |
### `kalshi_get_trades`
@@ -223,8 +221,8 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 取引データとメタデータ |
| `trades` | array | 取引オブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_candlesticks`
@@ -244,8 +242,7 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ローソク足データとメタデータ |
| `candlesticks` | array | OHLC始値・高値・安値・終値ローソク足データの配列 |
### `kalshi_get_fills`
@@ -268,8 +265,8 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 約定データとメタデータ |
| `fills` | array | 約定/取引オブジェクトの配列 |
| `paging` | object | さらに結果を取得するためのページネーションカーソル |
### `kalshi_get_series_by_ticker`
@@ -285,8 +282,7 @@ IDを指定してKalshiから特定の注文の詳細を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | シリーズデータとメタデータ |
| `series` | object | 詳細を含むシリーズオブジェクト |
### `kalshi_get_exchange_status`
@@ -301,8 +297,7 @@ Kalshi取引所の現在のステータス取引と取引所のアクティ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 取引所ステータスデータとメタデータ |
| `status` | object | trading_activeとexchange_activeフラグを含む取引所ステータス |
### `kalshi_create_order`
@@ -336,8 +331,7 @@ Kalshi予測市場に新しい注文を作成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 作成功ステータス |
| `output` | object | 作成された注文データ |
| `order` | object | 作成された注文オブジェクト |
### `kalshi_cancel_order`
@@ -355,8 +349,8 @@ Kalshiで既存の注文をキャンセルする
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | キャンセルされた注文データ |
| `order` | object | キャンセルされた注文オブジェクト |
| `reducedBy` | number | キャンセルされた契約数 |
### `kalshi_amend_order`
@@ -384,8 +378,7 @@ Kalshiで既存の注文の価格または数量を変更する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 変更された注文データ |
| `order` | object | 変更された注文オブジェクト |
## 注意事項

View File

@@ -40,11 +40,11 @@ Polymarketから予測市場のリストをオプションのフィルタリン
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `closed` | string | いいえ | クローズ状態でフィルタリングtrue/false。アクティブなマーケットのみの場合はfalseを使用。 |
| `closed` | string | いいえ | クローズ状態でフィルタリングtrue/false。アクティブな市場のみの場合はfalseを使用。 |
| `order` | string | いいえ | ソートフィールドvolumeNum、liquidityNum、startDate、endDate、createdAt |
| `ascending` | string | いいえ | ソート方向昇順の場合はtrue、降順の場合はfalse |
| `tagId` | string | いいえ | タグIDでフィルタリング |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
#### 出力
@@ -84,7 +84,7 @@ Polymarketからイベントのリストを取得し、オプションでフィ
| `order` | string | いいえ | ソートフィールドvolume、liquidity、startDate、endDate、createdAt |
| `ascending` | string | いいえ | ソート方向昇順の場合はtrue、降順の場合はfalse |
| `tagId` | string | いいえ | タグIDでフィルタリング |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
#### 出力
@@ -120,7 +120,7 @@ Polymarketからマーケットのフィルタリング用の利用可能なタ
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
#### 出力
@@ -139,15 +139,15 @@ Polymarketでマーケット、イベント、プロフィールを検索する
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `query` | string | はい | 検索クエリ用語 |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 検索結果とメタデータ |
| `output` | object | 検索結果データとメタデータ |
### `polymarket_get_series`
@@ -157,7 +157,7 @@ Polymarketからシリーズ関連するマーケットグループを取
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
#### 出力
@@ -199,7 +199,7 @@ PolymarketからIDで特定のシリーズ関連する市場グループ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | オーダーブックデータとメタデータ |
| `output` | object | 注文台帳データとメタデータ |
### `polymarket_get_price`
@@ -217,7 +217,7 @@ PolymarketからIDで特定のシリーズ関連する市場グループ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 価格データとメタデータ |
| `output` | object | 市場価格データとメタデータ |
### `polymarket_get_midpoint`
@@ -272,7 +272,7 @@ PolymarketからIDで特定のシリーズ関連する市場グループ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 最終取引価格とメタデータ |
| `output` | object | 最終取引価格データとメタデータ |
### `polymarket_get_spread`
@@ -306,7 +306,7 @@ PolymarketからIDで特定のシリーズ関連する市場グループ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ティックサイズとメタデータ |
| `output` | object | 最小ティックサイズデータとメタデータ |
### `polymarket_get_positions`
@@ -336,15 +336,14 @@ Polymarketから取引履歴を取得する
| --------- | ---- | -------- | ----------- |
| `user` | string | いいえ | 取引をフィルタリングするユーザーウォレットアドレス |
| `market` | string | いいえ | 取引をフィルタリングするマーケットID |
| `limit` | string | いいえ | ページあたりの結果数(推奨25-50 |
| `limit` | string | いいえ | ページあたりの結果数(最大50 |
| `offset` | string | いいえ | ページネーションオフセット(この数の結果をスキップ) |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 取引データとメタデータ |
| `trades` | array | 取引オブジェクトの配列 |
## 注意事項

View File

@@ -342,19 +342,30 @@ SendGridからメールテンプレートを削除する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `messageId` | string | メールメッセージIDsend_mail |
| `message` | string | ステータスまたは成功メッセージ |
| `messageId` | string | メールメッセージID \(send_mail\) |
| `to` | string | 受信者のメールアドレス \(send_mail\) |
| `subject` | string | メールの件名 \(send_mail, create_template_version\) |
| `id` | string | リソースID |
| `jobId` | string | 非同期操作のジョブID |
| `email` | string | メールアドレス |
| `firstName` | string | 名 |
| `lastName` | string | 姓 |
| `jobId` | string | 非同期操作のジョブID |
| `email` | string | 連絡先のメールアドレス |
| `firstName` | string | 連絡先の名 |
| `lastName` | string | 連絡先の姓 |
| `createdAt` | string | 作成タイムスタンプ |
| `updatedAt` | string | 最終更新タイムスタンプ |
| `listIds` | json | 連絡先が所属するリストIDの配列 |
| `customFields` | json | カスタムフィールドの値 |
| `contacts` | json | 連絡先の配列 |
| `contactCount` | number | 連絡先の数 |
| `lists` | json | リストの配列 |
| `templates` | json | テンプレートの配列 |
| `message` | string | ステータスまたは成功メッセージ |
| `name` | string | リソース名 |
| `generation` | string | テンプレート生成方法 |
| `templates` | json | テンプレートの配列 |
| `generation` | string | テンプレート世代 |
| `versions` | json | テンプレートバージョンの配列 |
| `templateId` | string | テンプレートID |
| `active` | boolean | テンプレートバージョンがアクティブかどうか |
| `htmlContent` | string | HTML内容 |
| `plainContent` | string | プレーンテキスト内容 |
### `sendgrid_create_template_version`

View File

@@ -21,24 +21,28 @@ import { Image } from '@/components/ui/image'
エディタ、APIへのデプロイ、またはチャットへのデプロイエクスペリエンスから始まるすべてのものにはスタートブロックを使用します。イベント駆動型ワークフローには他のトリガーも利用可能です
<Cards>
<Card title="スタート" href="/triggers/start">
<Card title="Start" href="/triggers/start">
エディタ実行、APIデプロイメント、チャットデプロイメントをサポートする統合エントリーポイント
</Card>
<Card title="ウェブフック" href="/triggers/webhook">
外部ウェブフックペイロードを受信
<Card title="Webhook" href="/triggers/webhook">
外部のwebhookペイロードを受信
</Card>
<Card title="スケジュール" href="/triggers/schedule">
<Card title="Schedule" href="/triggers/schedule">
Cronまたは間隔ベースの実行
</Card>
<Card title="RSS Feed" href="/triggers/rss">
新しいコンテンツのRSSとAtomフィードを監視
</Card>
</Cards>
## クイック比較
| トリガー | 開始条件 |
|---------|-----------------|
| **スタート** | エディタ実行、APIへのデプロイリクエスト、またはチャットメッセージ |
| **スケジュール** | スケジュールブロックで管理されるタイマー |
| **ウェブフック** | 受信HTTPリクエスト時 |
| **Start** | エディタ実行、APIへのデプロイリクエスト、またはチャットメッセージ |
| **Schedule** | スケジュールブロックで管理されるタイマー |
| **Webhook** | 受信HTTPリクエスト時 |
| **RSS Feed** | フィードに新しいアイテムが公開された時 |
> スタートブロックは常に `input`、`conversationId`、および `files` フィールドを公開します。追加の構造化データには入力フォーマットにカスタムフィールドを追加してください。

View File

@@ -0,0 +1,49 @@
---
title: RSSフィード
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
RSSフィードブロックはRSSとAtomフィードを監視します - 新しいアイテムが公開されると、ワークフローが自動的にトリガーされます。
<div className="flex justify-center">
<Image
src="/static/blocks/rss.png"
alt="RSSフィードブロック"
width={500}
height={400}
className="my-6"
/>
</div>
## 設定
1. **RSSフィードブロックを追加** - RSSフィードブロックをドラッグしてワークフローを開始
2. **フィードURLを入力** - 任意のRSSまたはAtomフィードのURLを貼り付け
3. **デプロイ** - ワークフローをデプロイしてポーリングを有効化
デプロイ後、フィードは1分ごとに新しいアイテムをチェックします。
## 出力フィールド
| フィールド | 型 | 説明 |
|-------|------|-------------|
| `title` | string | アイテムのタイトル |
| `link` | string | アイテムのリンク |
| `pubDate` | string | 公開日 |
| `item` | object | すべてのフィールドを含む生のアイテム |
| `feed` | object | 生のフィードメタデータ |
マッピングされたフィールドに直接アクセスするか(`<rss.title>`)、任意のフィールドに生のオブジェクトを使用します(`<rss.item.author>`、`<rss.feed.language>`)。
## ユースケース
- **コンテンツ監視** - ブログ、ニュースサイト、または競合他社の更新を追跡
- **ポッドキャスト自動化** - 新しいエピソードが公開されたときにワークフローをトリガー
- **リリース追跡** - GitHubリリース、変更ログ、または製品アップデートを監視
- **ソーシャルアグリゲーション** - RSSフィードを公開しているプラットフォームからコンテンツを収集
<Callout>
RSSトリガーは、トリガーを保存した後に公開されたアイテムに対してのみ実行されます。既存のフィードアイテムは処理されません。
</Callout>

View File

@@ -27,14 +27,16 @@ curl -H "x-api-key: YOUR_API_KEY" \
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 60, // Sustained rate limit per minute
"maxBurst": 120, // Maximum burst capacity
"remaining": 118, // Current tokens available (up to maxBurst)
"resetAt": "..." // When tokens next refill
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
"requestsPerMinute": 200, // Sustained rate limit per minute
"maxBurst": 400, // Maximum burst capacity
"remaining": 398, // Current tokens available
"resetAt": "..." // When tokens next refill
}
},
"usage": {
@@ -46,7 +48,7 @@ curl -H "x-api-key: YOUR_API_KEY" \
}
```
**注意:** 响应正文中的速率限制是针对工作流执行。调用此 API 端点的速率限制在响应头中(`X-RateLimit-*`)。
**注意:** 速率限制使用令牌桶算法。`remaining` 可以超过 `requestsPerMinute` 达到 `maxBurst`,当您最近未使用全部配额时,允许突发流量。响应正文中的速率限制适用于工作流执行。调用此 API 端点的速率限制在响应头中(`X-RateLimit-*`)。
### 查询日志
@@ -110,13 +112,15 @@ curl -H "x-api-key: YOUR_API_KEY" \
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -190,13 +194,15 @@ curl -H "x-api-key: YOUR_API_KEY" \
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"requestsPerMinute": 60,
"maxBurst": 120,
"remaining": 118,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 398,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
@@ -482,17 +488,25 @@ curl -H "x-api-key: YOUR_API_KEY" \
## 速率限制
API 实现了速率限制以确保公平使用
API 使用 **令牌桶算法** 进行速率限制,在提供公平使用的同时允许突发流量
- **免费计划**:每分钟 10 次请求
- **专业计划**:每分钟 30 次请求
- **团队计划**:每分钟 60 次请求
- **企业计划**:自定义限制
| 计划 | 请求/分钟 | 突发容量 |
|------|-----------|----------|
| 免费 | 10 | 20 |
| 专业版 | 30 | 60 |
| 团队版 | 60 | 120 |
| 企业版 | 120 | 240 |
速率限制信息包含在响应标头中:
- `X-RateLimit-Limit`:每个窗口的最大请求数
- `X-RateLimit-Remaining`:当前窗口中剩余的请求数
- `X-RateLimit-Reset`:窗口重置时的 ISO 时间戳
**工作原理:**
- 令牌以 `requestsPerMinute` 的速率补充
- 空闲时最多可累积 `maxBurst` 个令牌
- 每个请求消耗 1 个令牌
- 突发容量允许处理流量高峰
速率限制信息包含在响应头中:
- `X-RateLimit-Limit`:每分钟请求数(补充速率)
- `X-RateLimit-Remaining`:当前可用令牌数
- `X-RateLimit-Reset`:令牌下次补充的 ISO 时间戳
## 示例:轮询新日志
@@ -541,7 +555,7 @@ async function pollLogs() {
setInterval(pollLogs, 30000);
```
## 示例:处理 Webhook
## 示例:处理 Webhooks
```javascript
import express from 'express';

View File

@@ -147,8 +147,20 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
{
"success": true,
"rateLimit": {
"sync": { "isLimited": false, "limit": 10, "remaining": 10, "resetAt": "2025-09-08T22:51:55.999Z" },
"async": { "isLimited": false, "limit": 50, "remaining": 50, "resetAt": "2025-09-08T22:51:56.155Z" },
"sync": {
"isLimited": false,
"requestsPerMinute": 25,
"maxBurst": 50,
"remaining": 50,
"resetAt": "2025-09-08T22:51:55.999Z"
},
"async": {
"isLimited": false,
"requestsPerMinute": 200,
"maxBurst": 400,
"remaining": 400,
"resetAt": "2025-09-08T22:51:56.155Z"
},
"authType": "api"
},
"usage": {
@@ -159,6 +171,11 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
}
```
**速率限制字段:**
- `requestsPerMinute`:持续速率限制(令牌以此速率补充)
- `maxBurst`:您可以累积的最大令牌数(突发容量)
- `remaining`:当前可用令牌数(最多可达 `maxBurst`
**响应字段:**
- `currentPeriodCost` 反映当前计费周期的使用情况
- `limit` 来源于个人限制(免费/专业)或组织池限制(团队/企业)
@@ -170,9 +187,9 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
| 计划 | 每月使用限制 | 速率限制(每分钟) |
|------|-------------------|-------------------------|
| **免费** | $10 | 5 同步, 10 异步 |
| **专业** | $100 | 10 同步, 50 异步 |
| **团队** | $500共享 | 50 同步, 100 异步 |
| **免费** | $10 | 5 同步10 异步 |
| **专业** | $100 | 10 同步50 异步 |
| **团队** | $500共享 | 50 同步100 异步 |
| **企业** | 自定义 | 自定义 |
## 计费模式
@@ -187,9 +204,9 @@ Sim 使用 **基础订阅 + 超额** 的计费模式:
- 使用超过 $20 → 月底支付超额部分
- 示例:$35 使用 = $20订阅+ $15超额
**团队计划($40//月):**
- 团队成员共享使用额度
- 超额部分根据团队总使用量计算
**团队计划($40/每席位/月):**
- 团队成员之间共享使用额度
- 超额费用根据团队总使用量计算
- 组织所有者收到一张账单
**企业计划:**
@@ -198,20 +215,20 @@ Sim 使用 **基础订阅 + 超额** 的计费模式:
### 阈值计费
当未结算的超额费用达到 $50 时Sim 会自动结算全部未结算金额。
当未计费的超额费用达到 $50 时Sim 会自动计费全额未计费金额。
**示例:**
- 第 10 天:$70 超额 → 立即结算 $70
- 第 15 天:额外使用 $35总计 $105→ 已结算,无需操作
- 第 20 天:再使用 $50总计 $155结算 $85→ 立即结算 $85
- 第 10 天:$70 超额 → 立即计费 $70
- 第 15 天:额外使用 $35总计 $105→ 已计费,无需操作
- 第 20 天:再使用 $50总计 $155计费 $85→ 立即计费 $85
这将大量的超额费用分散到整个月,而不是在周期结束时收到一张大账单。
将大量的超额费用分散到整个月,而不是在周期结束时收到一张大账单。
## 成本管理最佳实践
1. **定期监控**:经常检查您的使用仪表板,避免意外情况
2. **设定预算**:使用计划限制作为支出控制的护栏
3. **优化工作流程**:审查高成本的执行情况,优化提示或模型选择
3. **优化工作流程**:审查高成本的执行操作,优化提示或模型选择
4. **使用合适的模型**:根据任务需求匹配模型复杂度
5. **批量处理相似任务**:尽可能合并多个请求以减少开销

View File

@@ -49,8 +49,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 市场数据和元数据 |
| `markets` | array | 市场对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_market`
@@ -66,8 +66,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 市场数据和元数据 |
| `market` | object | 包含详细信息的市场对象 |
### `kalshi_get_events`
@@ -87,8 +86,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 事件数据和元数据 |
| `events` | array | 事件对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_event`
@@ -105,8 +104,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 事件数据和元数据 |
| `event` | object | 包含详细信息的事件对象 |
### `kalshi_get_balance`
@@ -123,8 +121,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 余额数据和元数据 |
| `balance` | number | 账户余额(以分为单位) |
| `portfolioValue` | number | 投资组合价值(以分为单位) |
| `balanceDollars` | number | 账户余额(以美元为单位) |
| `portfolioValueDollars` | number | 投资组合价值(以美元为单位) |
### `kalshi_get_positions`
@@ -146,8 +146,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 位置数据和元数据 |
| `positions` | array | 持仓对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_orders`
@@ -169,8 +169,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 订单数据和元数据 |
| `orders` | array | 订单对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_order`
@@ -188,8 +188,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 订单数据 |
| `order` | object | 包含详细信息的订单对象 |
### `kalshi_get_orderbook`
@@ -205,8 +204,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 订单簿数据和元数据 |
| `orderbook` | object | 包含买入/卖出报价的订单簿 |
### `kalshi_get_trades`
@@ -223,8 +221,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 交易数据和元数据 |
| `trades` | array | 交易对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_candlesticks`
@@ -244,8 +242,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 蜡烛图数据和元数据 |
| `candlesticks` | array | OHLC 蜡烛图数据的数组 |
### `kalshi_get_fills`
@@ -268,8 +265,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 填充数据和元数据 |
| `fills` | array | 成交/交易对象的数组 |
| `paging` | object | 用于获取更多结果的分页游标 |
### `kalshi_get_series_by_ticker`
@@ -285,8 +282,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 系列数据和元数据 |
| `series` | object | 包含详细信息的系列对象 |
### `kalshi_get_exchange_status`
@@ -301,8 +297,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 交易所状态数据和元数据 |
| `status` | object | 包含 trading_active 和 exchange_active 标志的交易所状态 |
### `kalshi_create_order`
@@ -336,8 +331,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 创建的订单数据 |
| `order` | object | 创建的订单对象 |
### `kalshi_cancel_order`
@@ -355,8 +349,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 已取消的订单数据 |
| `order` | object | 已取消的订单对象 |
| `reducedBy` | number | 已取消的合约数量 |
### `kalshi_amend_order`
@@ -384,8 +378,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 修改后的订单数据 |
| `order` | object | 修改后的订单对象 |
## 注意

View File

@@ -41,18 +41,17 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `closed` | string | 否 | 按关闭状态筛选 \(true/false\)。使用 false 仅显示活跃市场。 |
| `order` | string | 否 | 排序字段 \(例如volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | 否 | 排序方向 \(true 表示升序false 表示降序\) |
| `order` | string | 否 | 排序字段 \(例如volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | 否 | 排序方向 \(true 升序false 降序\) |
| `tagId` | string | 否 | 按标签 ID 筛选 |
| `limit` | string | 否 | 每页结果数量 \(推荐25-50\) |
| `limit` | string | 否 | 每页结果数量 \(最大 50\) |
| `offset` | string | 否 | 分页偏移量 \(跳过此数量的结果\) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | 布尔值 | 操作成功状态 |
| `output` | 对象 | 市场数据和元数据 |
| `markets` | array | 市场对象数组 |
### `polymarket_get_market`
@@ -69,8 +68,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 市场数据和元数据 |
| `market` | object | 包含详细信息的市场对象 |
### `polymarket_get_events`
@@ -81,18 +79,17 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `closed` | string | 否 | 按关闭状态筛选 \(true/false\)。使用 false 仅显示活跃事件。 |
| `order` | string | 否 | 排序字段 \(例如volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | 否 | 排序方向 \(true 表示升序false 表示降序\) |
| `order` | string | 否 | 排序字段 \(例如volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | 否 | 排序方向 \(true 升序false 降序\) |
| `tagId` | string | 否 | 按标签 ID 筛选 |
| `limit` | string | 否 | 每页结果数量 \(推荐25-50\) |
| `limit` | string | 否 | 每页结果数量 \(最大 50\) |
| `offset` | string | 否 | 分页偏移量 \(跳过此数量的结果\) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 事件数据和元数据 |
| `events` | 数组 | 事件对象的数组 |
### `polymarket_get_event`
@@ -109,8 +106,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 事件数据和元数据 |
| `event` | 对象 | 包含详细信息的事件对象 |
### `polymarket_get_tags`
@@ -120,15 +116,14 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `limit` | string | 否 | 每页结果数量 \(推荐25-50\) |
| `offset` | string | 否 | 分页偏移量 \(跳过此数量的结果\) |
| `limit` | 字符串 | 否 | 每页结果数量 \(最多 50\) |
| `offset` | 字符串 | 否 | 分页偏移量 \(跳过此数量的结果\) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 标签数据和元数据 |
| `tags` | 数组 | 包含 id、标签和 slug 的标签对象数组 |
### `polymarket_search`
@@ -138,16 +133,15 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `query` | string | 是 | 搜索查询词 |
| `limit` | string | 否 | 每页结果数量 \(推荐25-50\) |
| `offset` | string | 否 | 分页偏移量 \(跳过此数量的结果\) |
| `query` | 字符串 | 是 | 搜索查询词 |
| `limit` | 字符串 | 否 | 每页结果数量 \(最多 50\) |
| `offset` | 字符串 | 否 | 分页偏移量 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 搜索结果和元数据 |
| `results` | 对象 | 包含市场、事件和个人资料数组的搜索结果 |
### `polymarket_get_series`
@@ -157,15 +151,14 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `limit` | string | 否 | 每页结果数量 \(推荐25-50\) |
| `offset` | string | 否 | 分页偏移量 \(跳过此数量的结果\) |
| `limit` | 字符串 | 否 | 每页结果数量 \(最多 50\) |
| `offset` | 字符串 | 否 | 分页偏移量 \(跳过此数量的结果\) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 系列数据和元数据 |
| `series` | 数组 | 系列对象的数组 |
### `polymarket_get_series_by_id`
@@ -181,8 +174,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 系列数据和元数据 |
| `series` | 对象 | 包含详细信息的系列对象 |
### `polymarket_get_orderbook`
@@ -198,8 +190,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 订单簿数据和元数据 |
| `orderbook` | 对象 | 包含买入和卖出数组的订单簿 |
### `polymarket_get_price`
@@ -216,8 +207,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 价格数据和元数据 |
| `price` | 字符串 | 市场价格 |
### `polymarket_get_midpoint`
@@ -233,8 +223,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 中间价格数据和元数据 |
| `midpoint` | 字符串 | 中点价格 |
### `polymarket_get_price_history`
@@ -254,8 +243,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 价格历史数据和元数据 |
| `history` | 数组 | 包含时间戳 \(t\) 和价格 \(p\) 的价格历史条目数组 |
### `polymarket_get_last_trade_price`
@@ -271,8 +259,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 最新交易价格和元数据 |
| `price` | 字符串 | 最新交易价格 |
### `polymarket_get_spread`
@@ -288,8 +275,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 价差数据和元数据 |
| `spread` | 对象 | 包含买价和卖价的买卖价差 |
### `polymarket_get_tick_size`
@@ -305,8 +291,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 跳动单位和元数据 |
| `tickSize` | 字符串 | 最小价格变动单位 |
### `polymarket_get_positions`
@@ -323,8 +308,7 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 持仓数据和元数据 |
| `positions` | 数组 | 持仓对象的数组 |
### `polymarket_get_trades`
@@ -334,17 +318,16 @@ Polymarket 集成的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `user` | string | 否 | 用户钱包地址,用于筛选交易 |
| `market` | string | 否 | 市场 ID用于筛选交易 |
| `limit` | string | 否 | 每页结果数量推荐25-50 |
| `offset` | string | 否 | 分页偏移量跳过此数量的结果 |
| `user` | 字符串 | 否 | 用于筛选交易的用户钱包地址 |
| `market` | 字符串 | 否 | 用于筛选交易的市场 ID |
| `limit` | 字符串 | 否 | 每页结果数量 \(最多 50\) |
| `offset` | 字符串 | 否 | 分页偏移量 \(跳过此数量的结果\) |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 交易数据和元数据 |
| `trades` | 数组 | 交易对象的数组 |
## 注意事项

View File

@@ -342,19 +342,30 @@ SendGrid 的主要功能包括:
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `messageId` | string | 电子邮件消息 ID \(send_mail\) |
| `message` | string | 状态或成功消息 |
| `messageId` | string | 邮件消息 ID \(send_mail\) |
| `to` | string | 收件人邮箱地址 \(send_mail\) |
| `subject` | string | 邮件主题 \(send_mail, create_template_version\) |
| `id` | string | 资源 ID |
| `jobId` | string | 异步操作的作业 ID |
| `email` | string | 电子邮件地址 |
| `firstName` | string | 名字 |
| `lastName` | string | 姓氏 |
| `email` | string | 联系人邮箱地址 |
| `firstName` | string | 联系人名字 |
| `lastName` | string | 联系人姓氏 |
| `createdAt` | string | 创建时间戳 |
| `updatedAt` | string | 最后更新时间戳 |
| `listIds` | json | 联系人所属列表 ID 的数组 |
| `customFields` | json | 自定义字段值 |
| `contacts` | json | 联系人数组 |
| `contactCount` | number | 联系人数 |
| `contactCount` | number | 联系人数 |
| `lists` | json | 列表数组 |
| `templates` | json | 模板数组 |
| `message` | string | 状态或成功消息 |
| `name` | string | 资源名称 |
| `generation` | string | 模板生成方式 |
| `templates` | json | 模板数组 |
| `generation` | string | 模板生成 |
| `versions` | json | 模板版本数组 |
| `templateId` | string | 模板 ID |
| `active` | boolean | 模板版本是否激活 |
| `htmlContent` | string | HTML 内容 |
| `plainContent` | string | 纯文本内容 |
### `sendgrid_create_template_version`

View File

@@ -21,24 +21,28 @@ import { Image } from '@/components/ui/image'
使用 Start 块处理从编辑器、部署到 API 或部署到聊天的所有操作。其他触发器可用于事件驱动的工作流:
<Cards>
<Card title="Start" href="/triggers/start">
<Card title="开始" href="/triggers/start">
支持编辑器运行、API 部署和聊天部署的统一入口点
</Card>
<Card title="Webhook" href="/triggers/webhook">
接收外部 webhook 负载
</Card>
<Card title="Schedule" href="/triggers/schedule">
<Card title="计划" href="/triggers/schedule">
基于 Cron 或间隔的执行
</Card>
<Card title="RSS 源" href="/triggers/rss">
监控 RSS 和 Atom 源的新内容
</Card>
</Cards>
## 快速对比
| 触发器 | 启动条件 |
|---------|-----------------|
| **Start** | 编辑器运行、部署到 API 请求或聊天消息 |
| **Schedule** | 在 Schedule 块中管理的计时器 |
| **Webhook** | 收到入站 HTTP 请求 |
| **开始** | 编辑器运行、部署到 API 请求或聊天消息 |
| **计划** | 在计划块中管理的计时器 |
| **Webhook** | 收到入站 HTTP 请求 |
| **RSS 源** | 源中发布了新项目 |
> Start 块始终公开 `input`、`conversationId` 和 `files` 字段。通过向输入格式添加自定义字段来增加结构化数据。

View File

@@ -0,0 +1,49 @@
---
title: RSS 订阅源
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
RSS 订阅源模块监控 RSS 和 Atom 订阅源——当有新内容发布时,您的工作流会自动触发。
<div className="flex justify-center">
<Image
src="/static/blocks/rss.png"
alt="RSS 订阅源模块"
width={500}
height={400}
className="my-6"
/>
</div>
## 配置
1. **添加 RSS 订阅源模块** - 拖动 RSS 订阅源模块以开始您的工作流
2. **输入订阅源 URL** - 粘贴任意 RSS 或 Atom 订阅源的 URL
3. **部署** - 部署您的工作流以激活轮询
部署后,订阅源每分钟检查一次是否有新内容。
## 输出字段
| 字段 | 类型 | 描述 |
|-------|------|-------------|
| `title` | string | 内容标题 |
| `link` | string | 内容链接 |
| `pubDate` | string | 发布日期 |
| `item` | object | 包含所有字段的原始内容 |
| `feed` | object | 原始订阅源元数据 |
可以直接访问映射字段 (`<rss.title>`),或者使用原始对象访问任意字段 (`<rss.item.author>`, `<rss.feed.language>`)。
## 使用场景
- **内容监控** - 跟踪博客、新闻网站或竞争对手的更新
- **播客自动化** - 当新剧集发布时触发工作流
- **版本跟踪** - 监控 GitHub 发布、更新日志或产品更新
- **社交聚合** - 收集支持 RSS 订阅源的平台内容
<Callout>
RSS 触发器仅对您保存触发器后发布的内容生效。现有的订阅源内容不会被处理。
</Callout>

View File

@@ -4578,25 +4578,26 @@ checksums:
content/32: 7b5e2207a0d93fd434b92f2f290a8dd5
content/33: f950b8f58af1973a3e00393d860bce02
content/34: d5ff07fec9455183e1d93f7ddf1dab1b
content/35: 405286ad5695582bf752b48aeeef3928
content/36: 7bb928aba33a4013ad5f08487da5bbf9
content/37: dbbf313837f13ddfa4a8843d71cb9cc4
content/38: cf10560ae6defb8ee5da344fc6509f6e
content/39: c5dc6e5de6e45b17ee1f5eb567a18e2f
content/40: 332dab0588fb35dabb64b674ba6120eb
content/41: 714b3f99b0a8686bbb3434deb1f682b3
content/42: ba18ac99184b17d7e49bd1abdc814437
content/43: bed2b629274d55c38bd637e6a28dbc4a
content/44: 71487ae6f6fb1034d1787456de442e6d
content/45: 137d9874cf5ec8d09bd447f224cc7a7c
content/46: 6b5b4c3b2f98b8fc7dd908fef2605ce8
content/47: 3af6812662546ce647a55939241fd88e
content/48: 6a4d7f0ccb8c28303251d1ef7b3dcca7
content/49: 5dce779f77cc2b0abf12802a833df499
content/50: aa47ff01b631252f024eaaae0c773e42
content/51: 1266d1c7582bb617cdef56857be34f30
content/52: c2cef2688104adaf6641092f43d4969a
content/53: 089fc64b4589b2eaa371de7e04c4aed9
content/35: 5d2d85e082d9fdd3859fb5c788d5f9a3
content/36: 23a7de9c5adb6e07c28c23a9d4e03dc2
content/37: 7bb928aba33a4013ad5f08487da5bbf9
content/38: dbbf313837f13ddfa4a8843d71cb9cc4
content/39: cf10560ae6defb8ee5da344fc6509f6e
content/40: c5dc6e5de6e45b17ee1f5eb567a18e2f
content/41: 332dab0588fb35dabb64b674ba6120eb
content/42: 714b3f99b0a8686bbb3434deb1f682b3
content/43: ba18ac99184b17d7e49bd1abdc814437
content/44: bed2b629274d55c38bd637e6a28dbc4a
content/45: 71487ae6f6fb1034d1787456de442e6d
content/46: 137d9874cf5ec8d09bd447f224cc7a7c
content/47: 6b5b4c3b2f98b8fc7dd908fef2605ce8
content/48: 3af6812662546ce647a55939241fd88e
content/49: 6a4d7f0ccb8c28303251d1ef7b3dcca7
content/50: 5dce779f77cc2b0abf12802a833df499
content/51: aa47ff01b631252f024eaaae0c773e42
content/52: 1266d1c7582bb617cdef56857be34f30
content/53: c2cef2688104adaf6641092f43d4969a
content/54: 089fc64b4589b2eaa371de7e04c4aed9
722959335ba76c9d0097860e2ad5a952:
meta/title: 1f5b53b9904ec41d49c1e726e3d56b40
content/0: c2b41859d63a751682f0d9aec488e581
@@ -4638,8 +4639,8 @@ checksums:
content/5: a29eaf87ca102db3f76a230f3651e767
content/6: de14c379db04a8dec394bce98e250254
content/7: 342e6c2c1e62d359bb47afe2be506c9a
content/8: fda5abcc2ed8a952f1d71fdf5e9df3f0
content/9: 25390578c4e18022ac486438a39695f5
content/8: 87ae5ed07aa69cb7816ffbadb4abf3f7
content/9: 5f75d4e4c56a5e56fd4b51aaaadc12ca
content/10: 841c6cfb0d4eb481cdb81dd170220ea0
content/11: 5fedb9fbc9445df0bf90a6d47710b1a6
content/12: c0ac34bbe27e2ff5272773e755641b5b
@@ -4648,14 +4649,14 @@ checksums:
content/15: 7f611a62b68373a068eb5dee13e53416
content/16: b05fe494158f2a347ced83b8d91a1445
content/17: 7c3a4621fd5a0e235fa98d2d3b76c12f
content/18: a192e0ca07a6cad306fa96b9942b7db4
content/18: 8afd31ae50ad8c39689c25d9ffd530bc
content/19: 3304a33dfb626c6e2267c062e8956a9d
content/20: 6f09faac9d13188e039a75ddccebc214
content/21: a450f2a3dc78405c47859dd35084e385
content/22: c0ac34bbe27e2ff5272773e755641b5b
content/23: 747141da074489515b6173c29d273403
content/24: 620d49f6fa198176bf6e59685e246e2c
content/25: fbea753ec4fb70bb0f2b61005fd3db18
content/25: b83b4c7d69d3cee9bc6873fe3da4bdc3
content/26: 3304a33dfb626c6e2267c062e8956a9d
content/27: 59c0fe74307c15d310aa16b09e835ddd
content/28: 53b824557984ddecb9b8c3e695967bc9
@@ -4708,13 +4709,14 @@ checksums:
content/75: c13873edfa750ce6caa155ef2f3f1883
content/76: 0bc07e36a42f7e25d5426b6a9deaac02
content/77: 017c829dd54fde20fa9fac7712342236
content/78: ceba97287cca68b1291142a38175d6d8
content/79: 02072ea1e956490139dbe8bbb0bc3e16
content/80: 44871834b9cb423a9978843c08623979
content/81: 0b22ed8a7e64005c666505c48e09f715
content/82: 494dcadaea5e62eddd599700511ecee5
content/83: 8332b16a0bf7a4c862f5104e9ffeb98d
content/84: 90e2f984a874a8f954ddfd127ec8178a
content/78: 6a8e85d2a7c36c724f752b93471122ea
content/79: b8e19306e2311b611318f8f89bb67971
content/80: 9ddcf8728a2fb2f293894bef4ddc98b4
content/81: 836487497de0b58afd0abff2ae5f044a
content/82: 0b22ed8a7e64005c666505c48e09f715
content/83: 494dcadaea5e62eddd599700511ecee5
content/84: 8332b16a0bf7a4c862f5104e9ffeb98d
content/85: 90e2f984a874a8f954ddfd127ec8178a
0e322683b6d10e9fa8c9a17ff15a5fb1:
meta/title: a912b3c7fb996fefccb182cf5c4a3fbc
content/0: e1f8d4b13687e7d73b5b5fbb4cb6142d
@@ -5760,9 +5762,9 @@ checksums:
content/1: e71056df0f7b2eb3b2f271f21d0052cc
content/2: da2b445db16c149f56558a4ea876a5f0
content/3: cec18f48b2cd7974eb556880e6604f7f
content/4: c187ae3362455acfe43282399f0d163a
content/4: b200402d6a01ab565fd56d113c530ef6
content/5: 4c3a5708af82c1ee42a12d14fd34e950
content/6: 12a43b499c1e8bb06b050964053ebde3
content/6: 64fbd5b16f4cff18ba976492a275c05e
content/7: a28151eeb5ba3518b33809055b04f0f6
content/8: cffe5b901d78ebf2000d07dc7579533e
content/9: 73486253d24eeff7ac44dfd0c8868d87
@@ -47364,7 +47366,7 @@ checksums:
content/98: 371d0e46b4bd2c23f559b8bc112f6955
content/99: 23aaf026b4d450b5bd6047e3acfe9a71
content/100: bcadfc362b69078beee0088e5936c98b
content/101: 7dec1de52492e6d0575c136baa1a8f92
content/101: 0c881f3d4490d2505a6f2258481370cb
content/102: 22512b23811c21236e3ec39743e30267
content/103: e70757a620b6d53b8232652c6f224100
content/104: 371d0e46b4bd2c23f559b8bc112f6955
@@ -48104,105 +48106,105 @@ checksums:
content/10: bb5d0521074c58da3f6b997faef887ae
content/11: 147f49b8a7a56cb60c881117480a71fb
content/12: 371d0e46b4bd2c23f559b8bc112f6955
content/13: 64096864823f19e5e62d794315454652
content/13: 8630b3b5b2372b19370c72091300ad36
content/14: bcadfc362b69078beee0088e5936c98b
content/15: 7311861512fa50f2c937d080692151e8
content/15: a85d1bc2e2dcee136f814dc84e7596d9
content/16: fbb677a4902291738c3a1f8b9303fd4a
content/17: 840eb13be6ecb8f1cf652ae9ea3f3dc8
content/18: 371d0e46b4bd2c23f559b8bc112f6955
content/19: 3dca9d793f1cf0619733a4aef965f385
content/20: bcadfc362b69078beee0088e5936c98b
content/21: 4634d3f690b2b1977d4555b2d4eee739
content/21: d25c6e706996660030e1ce66bcdb9e7e
content/22: 25e7fef7953155abe4219e03058c2f94
content/23: 1ba7d90c16ff260798c13836a74aad6a
content/24: 371d0e46b4bd2c23f559b8bc112f6955
content/25: 0f201f6957d27eaf9d1c4b4eab6ec8f7
content/25: d08681763f016af95e9608b1bab4c437
content/26: bcadfc362b69078beee0088e5936c98b
content/27: a88bd6e0708b4c3017be42eabc480947
content/27: 97d28b81355d5c4baf0775ffde238873
content/28: 528f164231d5bf39fcd8849734efebde
content/29: 6f779ec439c0e4b8e42370a6d3db4a5e
content/30: 371d0e46b4bd2c23f559b8bc112f6955
content/31: dee5cd45e0dcd6d19acf9081d0c8c805
content/32: bcadfc362b69078beee0088e5936c98b
content/33: b6656b4d0cb132b838d73e758850e3f9
content/33: fdba9cfce336484ab03fc2ac32bffea9
content/34: 78c6c8168c1295ea5aaab803874a6795
content/35: 566a0073bd48010fdaeac9388c100042
content/36: 371d0e46b4bd2c23f559b8bc112f6955
content/37: 13677e745575edf438b603aa13bac889
content/37: 7ea05e4c143f6c0d96d4f99b783a56f4
content/38: bcadfc362b69078beee0088e5936c98b
content/39: edb564cb480f1375dbc22c1553b1abe4
content/39: 68e2f713a365644dd8b8cffb8e16413c
content/40: 887cc3ce330dc18a3cf13692f28a4ade
content/41: c9b49024bd518a859dc76fb30eb46f94
content/42: 371d0e46b4bd2c23f559b8bc112f6955
content/43: 6c107c32338f118f6849c576107480f4
content/43: aed6b4d347aef9692e345670dfa3e683
content/44: bcadfc362b69078beee0088e5936c98b
content/45: fb073b94e2c2ca29212d093d5b103b87
content/45: 5862b3ba004ec511ea7f1dd972145348
content/46: 5f54ceacaf53a1db8bda5399ae205831
content/47: 3401fea610db18780a989ea02d8841aa
content/48: 371d0e46b4bd2c23f559b8bc112f6955
content/49: 13677e745575edf438b603aa13bac889
content/49: 7ea05e4c143f6c0d96d4f99b783a56f4
content/50: bcadfc362b69078beee0088e5936c98b
content/51: 08aaa5e1b8c8e8c146f68228e6d53792
content/51: 30f09d8ee99a42c072c44db4703a91a4
content/52: 19a1b9442970caf8758d0ffc7f179574
content/53: 10a83dde0830cd58d4cb19dd8a0c3649
content/54: 371d0e46b4bd2c23f559b8bc112f6955
content/55: c0d2832eacdb9bd8c8d9156019956cf7
content/56: bcadfc362b69078beee0088e5936c98b
content/57: 08aaa5e1b8c8e8c146f68228e6d53792
content/57: d05aa3c271674d80fd34fe5d7c0378fd
content/58: 17afc458eef6484867c139fd212d90d9
content/59: da1f2733213d33e0509dc557bc6c80a8
content/60: 371d0e46b4bd2c23f559b8bc112f6955
content/61: 42c3b4d062ae50f6ebaf9a7460a6fc5e
content/62: bcadfc362b69078beee0088e5936c98b
content/63: d6ec19129a153bc0667e76edb9c2953d
content/63: 35bbaf81608917e5e0f8e1822fd2d3f8
content/64: cc46a76f9ce04f5557e34fca2d6562c1
content/65: 7c3b126537de352cabfc495892892c1c
content/66: 371d0e46b4bd2c23f559b8bc112f6955
content/67: 954008ba49a8401ae8a22f7092e6ca23
content/68: bcadfc362b69078beee0088e5936c98b
content/69: b3764199e9c88e86cd0fe6935e539a25
content/69: e343008f5f0c6e61e389fd7ba47bd183
content/70: c635592e55ceed2da66ad0aa9c5f0ee5
content/71: 90bf1835ccec7f5ee9b7d5ffb3965654
content/72: 371d0e46b4bd2c23f559b8bc112f6955
content/73: 42c3b4d062ae50f6ebaf9a7460a6fc5e
content/74: bcadfc362b69078beee0088e5936c98b
content/75: 27eb346aea67fe81ed94fee3fa0e8bdf
content/75: 3b6142572f73f6f6a58951d0048010f5
content/76: a020f0e3fe6a3496a735851f230bce15
content/77: ca39beca950791eb4a75a8da900114bf
content/78: 371d0e46b4bd2c23f559b8bc112f6955
content/79: 2f5b4e0859d5314f6bf40be3ac13f641
content/80: bcadfc362b69078beee0088e5936c98b
content/81: bc8fff0a53b034c9383de92b55143450
content/81: 2c579b11630f6b898e520e734588c2a2
content/82: 67b79204dbe622e540a879638ef27f02
content/83: aed07dcfb7bcbcb4f8e1297276888f04
content/84: 371d0e46b4bd2c23f559b8bc112f6955
content/85: 42c3b4d062ae50f6ebaf9a7460a6fc5e
content/86: bcadfc362b69078beee0088e5936c98b
content/87: 4e1b6f9d3f1d760834a31d8b49faa01e
content/87: 4373349c4b00e00a7efa918a02fd0859
content/88: e0e21aa31b6b03674fb8e6a9167d809d
content/89: 6d55eb60d0264d211cd8e027885738fa
content/90: 371d0e46b4bd2c23f559b8bc112f6955
content/91: 42c3b4d062ae50f6ebaf9a7460a6fc5e
content/92: bcadfc362b69078beee0088e5936c98b
content/93: a31e86b6e03b7df7b9bd013f5c5b3d20
content/93: 5050b0f0d60755c98d93630e06da0c72
content/94: 8629b9c6f2ecbbacb248f9a65938555e
content/95: f69ca030edb0f0c9970a51250fbe1699
content/96: 371d0e46b4bd2c23f559b8bc112f6955
content/97: 42c3b4d062ae50f6ebaf9a7460a6fc5e
content/98: bcadfc362b69078beee0088e5936c98b
content/99: e2fc3018c16f1ae4f3e907fc33548d46
content/99: cdaf2dce3d06cfa70cf438b12e769007
content/100: e5d8665486fab89d1c60b9ab6cfe1c2e
content/101: 62c4501130fb887500df574e0c09c20d
content/102: 371d0e46b4bd2c23f559b8bc112f6955
content/103: 7e048124d93bb4cdcd6effb9b0c4b414
content/104: bcadfc362b69078beee0088e5936c98b
content/105: c463989e1754b17b2cc1a7b9fff68cb6
content/105: 6fe8446ecb76f21e5fa718b65e7ed421
content/106: 0b68d1e5df6bd6d9ea8be971c4f8bbbf
content/107: 13876fa5c17849a15f6d54647feb6ae5
content/108: 371d0e46b4bd2c23f559b8bc112f6955
content/109: 77a4b3016c40c377897e029411f1b8fd
content/109: 1824d8209db86f3c1fa3f5beed26e05c
content/110: bcadfc362b69078beee0088e5936c98b
content/111: 77916aef5f86264c96b85ab874c3c447
content/111: b3ec56f51a86f8eb02fa471b377171c5
content/112: b3f310d5ef115bea5a8b75bf25d7ea9a
content/113: 29208f859f7c25898a8bb435d3e744d0
54ec89df9800159df913e0955d2775e1:
@@ -48222,103 +48224,103 @@ checksums:
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 78b6dd9f0a02aecf0d26bb37cd9a6854
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 7311861512fa50f2c937d080692151e8
content/14: 45e4b48c07557ffff5956d9466295197
content/15: a29e8f758eeb0adc9d14cb3ecc9a3572
content/16: 3bb1094fb4f6bbacf4503db791290728
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: d185d2b7ceafeacce0f677a4649cb308
content/19: bcadfc362b69078beee0088e5936c98b
content/20: 4634d3f690b2b1977d4555b2d4eee739
content/20: d25c6e706996660030e1ce66bcdb9e7e
content/21: dd173220664de67767b3f0aad2808436
content/22: 059484693528bd0f6324763d7f32abeb
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: dfb2c2a7b6f5ddb86f9104defb6e8b5d
content/25: bcadfc362b69078beee0088e5936c98b
content/26: a88bd6e0708b4c3017be42eabc480947
content/26: 8dbf76672372fa4f655305255bab0568
content/27: 49e6762a44cc519ca70217ba8d90fbf7
content/28: 77451909a80759c6ebb246a526e6fd7b
content/29: 371d0e46b4bd2c23f559b8bc112f6955
content/30: 6edd84776ad45703537015af58f492cb
content/31: bcadfc362b69078beee0088e5936c98b
content/32: b6656b4d0cb132b838d73e758850e3f9
content/32: fdba9cfce336484ab03fc2ac32bffea9
content/33: a04ec00949bbb0a7c99bff8fd232e820
content/34: 3b42cf913c5f403544540b6244dc8e2e
content/35: 371d0e46b4bd2c23f559b8bc112f6955
content/36: bddd30707802c07aac61620721bfaf16
content/37: bcadfc362b69078beee0088e5936c98b
content/38: b622395040bf1aef193558d10a38c128
content/38: fa2c581e6fb204f5ddbd0ffcbf0f7123
content/39: 65de097e276f762b71d59fa7f9b0a207
content/40: 013f52c249b5919fdb6d96700b25f379
content/41: 371d0e46b4bd2c23f559b8bc112f6955
content/42: 8c33b1b3f984f011679b36b77801b936
content/43: bcadfc362b69078beee0088e5936c98b
content/44: c463989e1754b17b2cc1a7b9fff68cb6
content/44: af1f849f131fe114138b638d3dc9edd7
content/45: 353bd4e36dd70f288ff051157c0bbadb
content/46: 9ee4b0cc8813aeea6b991dabb05c0b2b
content/47: 371d0e46b4bd2c23f559b8bc112f6955
content/48: 93c01d42da765b2862a1c90e5c207f3a
content/49: bcadfc362b69078beee0088e5936c98b
content/50: cb25800f3f5e9a5688769ba1f1d101fb
content/50: 6af34cd425be8517ea6fb6a014621329
content/51: 0440d7abaca1a48b2ce63b48c97444a1
content/52: 4cb6ac8a34a8cc0d87a85a45dd9382b9
content/53: 371d0e46b4bd2c23f559b8bc112f6955
content/54: df651aea48c483f6163ae49e4f1fda5a
content/55: bcadfc362b69078beee0088e5936c98b
content/56: 90bd63383b8ba686aa71a192a945344a
content/56: cb3c2f1293d213df984c1273c5a4d14b
content/57: 0c321044b21a59eec7b04f91d6ad521f
content/58: d8a0a8ae22b5c2a0664f1172adf2c1d1
content/59: 371d0e46b4bd2c23f559b8bc112f6955
content/60: fcef3a88d9de7bfa5e308e4cece9136e
content/61: bcadfc362b69078beee0088e5936c98b
content/62: d6ec19129a153bc0667e76edb9c2953d
content/62: b7ed771bffa972a584913797325ee02b
content/63: ce48c0560ca3ddaad402f2bdd9d459f3
content/64: d96d5d108333391af5bc3ab6425c9f29
content/65: 371d0e46b4bd2c23f559b8bc112f6955
content/66: be92911dda52b245d2fda5b0c54527dc
content/67: bcadfc362b69078beee0088e5936c98b
content/68: 77916aef5f86264c96b85ab874c3c447
content/68: 43b7bd9c584decffb62fdc75409675a6
content/69: 85eba1d62b7cdd083d0a4991169d7162
content/70: d4dd284c2b688770d5c732a7e6dff3f0
content/71: 371d0e46b4bd2c23f559b8bc112f6955
content/72: ad3910637d3e53fef449e8a5aa74b1ef
content/73: bcadfc362b69078beee0088e5936c98b
content/74: 3f6664c231057ceadd9ac8cba5332b3e
content/74: 56a1ecf365a95c06e42fcf80dc28fb31
content/75: 4936debbcf6ba547fa7c9265f7dcf40f
content/76: 17a43ca41485e2f78ef7987c78a52e8b
content/77: 371d0e46b4bd2c23f559b8bc112f6955
content/78: b87a8359c3277447f29050ef280f4ceb
content/79: bcadfc362b69078beee0088e5936c98b
content/80: be30e8adfbab208f4b7b6fedc0559c19
content/80: a1414f8625473aca0b4635ebd8e50c34
content/81: 72de310caecaf16e8e74ff37a7fa46b0
content/82: 4738fdcfd44aa60f5bfd7f484be36360
content/83: 371d0e46b4bd2c23f559b8bc112f6955
content/84: 3ff5cc143dcf2514e730b4f68757a6cb
content/85: bcadfc362b69078beee0088e5936c98b
content/86: 08aaa5e1b8c8e8c146f68228e6d53792
content/86: d05aa3c271674d80fd34fe5d7c0378fd
content/87: a72a19e1aff8ea71d24df98f312d9ada
content/88: 0a0fecc6d1a70497410160ee0ea9e757
content/89: 371d0e46b4bd2c23f559b8bc112f6955
content/90: d71b6bb8e2dd6ce98101aec6a1dd77f2
content/91: bcadfc362b69078beee0088e5936c98b
content/92: 2e1b6ed9ba71ee98540f582bf5decd41
content/92: 55235554f8ac9d06bebf6827bb5a67a4
content/93: b37060e61c0433052dd02939aa60e412
content/94: c2184e718b1015489f82854fe48e6a37
content/95: 371d0e46b4bd2c23f559b8bc112f6955
content/96: 574cc410d9df4f37e7dc291491e13fe6
content/97: bcadfc362b69078beee0088e5936c98b
content/98: b06f1d576f491777f39a47401c77f268
content/98: 7f3282d2c35a26a84c97a5c8aec51bd8
content/99: 5070ca6b1d2cbd4f8450b15e8f46e446
content/100: 3edfc5e9d7138730bb73863616f3435e
content/101: 371d0e46b4bd2c23f559b8bc112f6955
content/102: 8d5f23e56e4606bacc7ac1f3b1cbe86b
content/103: bcadfc362b69078beee0088e5936c98b
content/104: 52f075cb7459e5cf8daf1f3e37ecd36b
content/104: c3013a47beb9b8db755de36615056c13
content/105: 82c709054f9e80781baa7d678ff03b24
content/106: 5a38d9ff2a65641f8f13e91db263cc9e
content/107: 371d0e46b4bd2c23f559b8bc112f6955
content/108: 71b47f694d04e8f1a8fa22723539fe47
content/109: bcadfc362b69078beee0088e5936c98b
content/110: 45b9563e3bcc50f89b39b015a9d46114
content/110: 4a0585b138348fe3806097c0542e8cb2
content/111: b3f310d5ef115bea5a8b75bf25d7ea9a
content/112: cbeffb4f92b38461ac320bc6fe7f7ef0
d58d2e8b125a994bcfb20ff98cfef0ad:
@@ -49300,3 +49302,17 @@ checksums:
content/42: dc2cfed837ea55adfa23bd7c87d5299d
content/43: b3f310d5ef115bea5a8b75bf25d7ea9a
content/44: df2ef65659b8ea0a13916358943f965b
ebed3bd73520bf81399749586796f9d0:
meta/title: 1763bebd6001500cdfc1b5127b0c1cde
content/0: eb0ed7078f192304703144f4cac3442f
content/1: ba5ba29787a0eb35c46dacb3544bafe1
content/2: 5ed74bf0e91235f71eeceb25712ad2d3
content/3: 0441638444240cd20a6c69ea1d3afbb1
content/4: ef102e10f1402df7290680c1e9df8a5e
content/5: 95afa83a30cb01724b932b19dd69f20b
content/6: 8ebc5e005f61d253c006824168abaf22
content/7: df81a49b54d378523fb74aa0b0fb8be1
content/8: c5fb77d31bae86aa85f2b2b84ce0beab
content/9: 7a3be8a3771ee428ecf09008e42c0e2e
content/10: 42e4caf9b036a8d7726a8968f3ed201f
content/11: e74f8ee79105babdaa8dfec520ecdf74

View File

@@ -13,6 +13,7 @@
"dependencies": {
"@tabler/icons-react": "^3.31.0",
"@vercel/og": "^0.6.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "16.2.3",
"fumadocs-mdx": "14.1.0",

View File

@@ -1,777 +0,0 @@
# Sim App Architecture Guidelines
You are building features in the Sim app following the architecture. This file defines the patterns, structures, and conventions you must follow.
---
## Core Principles
1. **Single Responsibility Principle**: Each component, hook, and store should have one clear purpose
2. **Composition Over Complexity**: Break down complex logic into smaller, composable pieces
3. **Type Safety First**: Use TypeScript interfaces for all props, state, and return types
4. **Predictable State Management**: Use Zustand for global state, local state for UI-only concerns
5. **Performance by Default**: Use useMemo, useCallback, and refs appropriately
6. **Accessibility**: Include semantic HTML and ARIA attributes
7. **Documentation**: Use TSDoc for all public interfaces and keep it in sync with code changes
---
## File Organization
### Directory Structure
```
feature/
├── components/ # Feature components
│ ├── sub-feature/ # Sub-feature with its own components
│ │ ├── component.tsx
│ │ └── index.ts
├── hooks/ # Custom hooks for feature logic
│ ├── use-feature-logic.ts
│ └── use-another-hook.ts
└── feature.tsx # Main feature component
```
### Naming Conventions
- **Components**: PascalCase with descriptive names (`WorkflowList`, `TriggerPanel`)
- **Hooks**: camelCase with `use` prefix (`useWorkflowOperations`, `usePanelResize`)
- **Files**: kebab-case matching export name (`workflow-list.tsx`, `use-panel-resize.ts`)
- **Stores**: kebab-case in stores/ directory (`sidebar/store.ts`, `workflows/registry/store.ts`)
- **Constants**: SCREAMING_SNAKE_CASE at module level
- **Interfaces**: PascalCase with descriptive suffix (`WorkflowListProps`, `UseWorkspaceManagementProps`)
---
## Component Architecture
### Component Structure Template
```typescript
'use client' // Only if using hooks like useState, useEffect, etc.
import { useCallback, useMemo, useRef, useState } from 'react'
// Other imports organized: external, internal paths, relative
/**
* Constants - Define at module level before component
*/
const DEFAULT_VALUE = 100
const MIN_VALUE = 50
const MAX_VALUE = 200
const CONFIG = {
SPACING: 8,
ITEM_HEIGHT: 25,
} as const
interface ComponentProps {
/** Description of prop */
requiredProp: string
/** Description with default noted */
optionalProp?: boolean
onAction?: (id: string) => void
}
/**
* Component description explaining purpose and key features.
* Mention important integrations, hooks, or patterns used.
*
* @param props - Component props
* @returns JSX description
*/
export function ComponentName({
requiredProp,
optionalProp = false,
onAction,
}: ComponentProps) {
// 1. Refs first
const containerRef = useRef<HTMLDivElement>(null)
// 2. External hooks (router, params, context)
const params = useParams()
// 3. Store hooks
const { state, actions } = useStore()
// 4. Custom hooks (your feature hooks)
const { data, isLoading } = useCustomHook({ requiredProp })
// 5. Local state (UI-only concerns)
const [isOpen, setIsOpen] = useState(false)
// 6. Derived/computed values with useMemo
const filteredData = useMemo(() => {
return data.filter(item => item.active)
}, [data])
// 7. Callbacks with useCallback
const handleClick = useCallback((id: string) => {
onAction?.(id)
}, [onAction])
// 8. Effects
useEffect(() => {
// Setup logic
return () => {
// Cleanup logic
}
}, [])
// 9. Render helpers (if complex)
const renderItem = useCallback((item: Item) => (
<div key={item.id}>{item.name}</div>
), [])
// 10. Return JSX
return (
<div ref={containerRef} className='...' aria-label='...'>
{/* Section comments for clarity */}
{/* Header */}
<header>...</header>
{/* Content */}
<main>...</main>
</div>
)
}
```
### Component Rules
1. **Client Components**: Add `'use client'` directive when using React hooks
2. **Props Interface**: Always define TypeScript interface, even for simple props
3. **TSDoc Required and Up-to-Date**: Include description, @param, and @returns. Update TSDoc whenever props, behavior, or side effects change (including additions and deletions).
4. **Constants**: Extract magic numbers and config to module-level constants using `as const`
5. **No Inline Styles**: Use Tailwind classes exclusively (CSS variables for dynamic values)
6. **Section Comments**: Use comments to mark logical sections of JSX
7. **Semantic HTML**: Use appropriate elements (`aside`, `nav`, `article`, etc.)
8. **ARIA Attributes**: Include `aria-label`, `aria-orientation`, `role` where appropriate
9. **Refs for DOM**: Use refs for direct DOM access, not state
10. **Callback Props**: Always use optional chaining for callback props (`onAction?.(...)`)
---
## Custom Hooks Architecture
### Hook Structure Template
```typescript
import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('useFeatureName')
/**
* Constants specific to this hook
*/
const DEFAULT_CONFIG = {
timeout: 1000,
retries: 3,
} as const
interface UseFeatureNameProps {
/** Description of required prop */
id: string
/** Optional callback fired on success */
onSuccess?: (result: Result) => void
}
/**
* Custom hook to [clear description of purpose].
* [Additional context about what it manages or coordinates].
*
* @param props - Configuration object containing id and callbacks
* @returns Feature state and operations
*/
export function useFeatureName({ id, onSuccess }: UseFeatureNameProps) {
// 1. Refs (to avoid dependency issues)
const idRef = useRef(id)
const onSuccessRef = useRef(onSuccess)
// 2. State
const [data, setData] = useState<Data | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
// 3. Update refs when values change
useEffect(() => {
idRef.current = id
onSuccessRef.current = onSuccess
}, [id, onSuccess])
// 4. Operations with useCallback (stable references)
const fetchData = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`/api/data/${idRef.current}`)
const result = await response.json()
setData(result)
onSuccessRef.current?.(result)
logger.info('Data fetched successfully', { id: idRef.current })
} catch (err) {
const error = err as Error
setError(error)
logger.error('Failed to fetch data', { error })
} finally {
setIsLoading(false)
}
}, []) // Empty deps because using refs
const updateData = useCallback(async (newData: Partial<Data>) => {
try {
logger.info('Updating data', { id: idRef.current, newData })
const response = await fetch(`/api/data/${idRef.current}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newData),
})
const result = await response.json()
setData(result)
return true
} catch (err) {
logger.error('Failed to update data', { error: err })
return false
}
}, [])
// 5. Effects
useEffect(() => {
if (id) {
fetchData()
}
}, [id, fetchData])
// 6. Return object - group by state and operations
return {
// State
data,
isLoading,
error,
// Operations
fetchData,
updateData,
}
}
```
### Hook Rules
1. **Single Responsibility**: Each hook manages one concern (data fetching, resize, navigation)
2. **Props Interface**: Define TypeScript interface for all parameters
3. **TSDoc Required and Up-to-Date**: Include clear description, @param, and @returns. Update TSDoc whenever inputs, outputs, behavior, or side effects change (including additions and deletions).
4. **Logger Usage**: Import and use logger instead of console.log
5. **Refs for Stable Deps**: Use refs to avoid recreating callbacks unnecessarily
6. **useCallback Always**: Wrap all returned functions in useCallback
7. **Grouped Returns**: Return object with comments separating State and Operations
8. **Error Handling**: Always try/catch async operations and log errors
9. **Loading States**: Track loading, error states for async operations
10. **Dependency Arrays**: Be explicit and correct with all dependency arrays
---
## Store Architecture (Zustand)
### Store Structure Template
```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
/**
* Store state interface
*/
interface FeatureState {
// State properties
items: Item[]
activeId: string | null
isLoading: boolean
// Actions
setItems: (items: Item[]) => void
setActiveId: (id: string | null) => void
addItem: (item: Item) => void
removeItem: (id: string) => void
clearState: () => void
}
/**
* Constants - Configuration values
*/
const DEFAULT_CONFIG = {
maxItems: 100,
cacheTime: 3600,
} as const
/**
* Initial state factory
*/
const createInitialState = () => ({
items: [],
activeId: null,
isLoading: false,
})
/**
* Feature store managing [description].
* [Additional context about what this store coordinates].
*/
export const useFeatureStore = create<FeatureState>()(
persist(
(set, get) => ({
...createInitialState(),
setItems: (items) => set({ items }),
setActiveId: (id) => set({ activeId: id }),
addItem: (item) =>
set((state) => ({
items: [...state.items, item].slice(-DEFAULT_CONFIG.maxItems),
})),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
activeId: state.activeId === id ? null : state.activeId,
})),
clearState: () => set(createInitialState()),
}),
{
name: 'feature-state',
// Optionally customize what to persist
partialize: (state) => ({
items: state.items,
activeId: state.activeId,
}),
onRehydrateStorage: () => (state) => {
// Validate and transform persisted state if needed
if (state) {
// Enforce constraints
if (state.items.length > DEFAULT_CONFIG.maxItems) {
state.items = state.items.slice(-DEFAULT_CONFIG.maxItems)
}
}
},
}
)
)
```
### Store Rules
1. **Interface First**: Define TypeScript interface including both state and actions
2. **Constants**: Extract configuration values to module-level constants
3. **TSDoc on Store**: Document what the store manages
4. **Persist Strategically**: Only persist what's needed across sessions
5. **Validation**: Use onRehydrateStorage to validate persisted state
6. **Immutable Updates**: Use set() with new objects/arrays, never mutate
7. **Derived State**: Use getters or selectors, not stored computed values
8. **CSS Variables**: Update CSS variables in setters for hydration-safe dynamic styles
9. **Cleanup Actions**: Provide clear/reset actions for state cleanup
10. **Functional Updates**: Use `set((state) => ...)` when new state depends on old state
---
## State Management Strategy
### When to Use Local State (useState)
- UI-only concerns (dropdown open, hover states, form inputs)
- Component-scoped state not needed elsewhere
- Temporary state that doesn't need persistence
### When to Use Zustand Store
- Shared state across multiple components
- State that needs persistence (localStorage)
- Global application state (user, theme, settings)
- Complex state with many actions/reducers
### When to Use Refs (useRef)
- DOM element references
- Avoiding dependency issues in hooks
- Storing mutable values that don't trigger re-renders
- Accessing latest props/state in callbacks without recreating them
---
## CSS and Styling
### CSS Variables for Dynamic Styles
Use CSS variables for values that need to persist across hydration:
```typescript
// In store setter
setSidebarWidth: (width) => {
const clampedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
set({ sidebarWidth: clampedWidth })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {
document.documentElement.style.setProperty('--sidebar-width', `${clampedWidth}px`)
}
}
// In component
<aside
className='sidebar-container'
style={{ width: 'var(--sidebar-width)' }}
>
```
### Tailwind Classes
1. **No Inline Styles**: Use Tailwind utility classes exclusively
2. **Dark Mode**: Include dark mode variants only when the value differs from light mode
3. **No Duplicate Dark Classes**: Never add a `dark:` class when the value is identical to the light mode class (e.g., `text-[var(--text-primary)] dark:text-[var(--text-primary)]` is redundant - just use `text-[var(--text-primary)]`)
4. **Exact Values**: Use exact values from design system (`text-[14px]`, `h-[25px]`)
5. **cn for Conditionals**: Use `cn()` from `@/lib/utils` for conditional classes (wraps clsx + tailwind-merge for conflict resolution)
6. **Consistent Spacing**: Use spacing tokens (`gap-[8px]`, `px-[14px]`)
7. **Transitions**: Add transitions for interactive states (`transition-colors`)
8. **Prefer px units**: Use arbitrary px values over scale utilities (e.g., `px-[4px]` instead of `px-1`)
```typescript
import { cn } from '@/lib/utils'
<div
className={cn(
'base-classes that-always-apply',
isActive && 'active-state-classes',
disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-accent'
)}
>
```
---
## TypeScript Patterns
### Interface Conventions
```typescript
// Component props
interface ComponentNameProps {
requiredProp: string
optionalProp?: boolean
}
// Hook props
interface UseHookNameProps {
id: string
onSuccess?: () => void
}
// Store state
interface FeatureState {
data: Data[]
isLoading: boolean
actions: () => void
}
// Return types (if complex)
interface UseHookNameReturn {
state: State
actions: Actions
}
```
### Type Safety Rules
1. **No `any`**: Use proper types or `unknown` with type guards
2. **Props Interface**: Always define, even for simple components
3. **Callback Types**: Define full signature including parameters and return type
4. **Generic Types**: Use generics for reusable components/hooks
5. **Const Assertions**: Use `as const` for constant objects/arrays
6. **Type Guards**: Create type guards for runtime checks
7. **Ref Types**: Explicitly type refs (`useRef<HTMLDivElement>(null)`)
---
## Performance Patterns
### Memoization
```typescript
// useMemo for expensive computations
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items])
// useCallback for functions passed as props
const handleClick = useCallback((id: string) => {
onItemClick?.(id)
}, [onItemClick])
// useCallback for render functions
const renderItem = useCallback((item: Item) => (
<ItemComponent key={item.id} item={item} onClick={handleClick} />
), [handleClick])
```
### When to Memoize
1. **useMemo**: Expensive calculations, filtering/sorting large arrays, object creation in render
2. **useCallback**: Functions passed to child components, dependencies in other hooks, event handlers used in effects
3. **Don't Over-Memoize**: Simple calculations, primitives, or functions not passed down
### Refs for Avoiding Recreations
```typescript
// Pattern: Use refs to avoid function recreations
const onSuccessRef = useRef(onSuccess)
useEffect(() => {
onSuccessRef.current = onSuccess
}, [onSuccess])
const stableCallback = useCallback(() => {
// Use ref so this callback never needs to change
onSuccessRef.current?.()
}, []) // Empty deps!
```
---
## Logging and Debugging
### Logger Usage
```typescript
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('ComponentName')
// Use throughout component/hook
logger.info('User action', { userId, action })
logger.warn('Potential issue', { details })
logger.error('Operation failed', { error })
```
### Logging Rules
1. **No console.log**: Use logger.info/warn/error instead
2. **Logger Per File**: Create logger with component/hook name
3. **Structured Logging**: Pass objects with context, not just strings
4. **Log Levels**:
- `info`: Normal operations, user actions, state changes
- `warn`: Unusual but handled situations, deprecations
- `error`: Failures, exceptions, errors that need attention
---
## Linting and Formatting
### Automated Linting
**Do not manually fix linting errors.** The project uses automated linting tools that should handle formatting and style issues.
### Rules
1. **No Manual Fixes**: Do not attempt to manually reorder CSS classes, fix formatting, or address linter warnings
2. **Use Automated Tools**: If linting errors need to be fixed, run `bun run lint` to let the automated tools handle it
3. **Focus on Logic**: Concentrate on functionality, TypeScript correctness, and architectural patterns
4. **Let Tools Handle Style**: Biome and other linters will automatically format code according to project standards
### When Linting Matters
- **Syntax Errors**: Fix actual syntax errors that prevent compilation
- **Type Errors**: Address TypeScript type errors that indicate logic issues
- **Ignore Style Warnings**: CSS class order, formatting preferences, etc. will be handled by tooling
```bash
# If linting is required
bun run lint
```
---
## Code Quality Checklist
Before considering a component/hook complete, verify:
### Documentation
- [ ] TSDoc in sync with implementation after any change (params/returns/behavior/throws)
- [ ] TSDoc comment on component/hook/store
- [ ] Props interface documented with /** */ comments
- [ ] Complex logic explained with inline comments
- [ ] Section comments in JSX for clarity
### TypeScript
- [ ] All props have interface defined
- [ ] No `any` types used
- [ ] Refs properly typed
- [ ] Return types explicit for complex hooks
### Performance
- [ ] useMemo for expensive computations
- [ ] useCallback for functions passed as props
- [ ] Refs used to avoid unnecessary recreations
- [ ] No unnecessary re-renders
### Hooks
- [ ] Correct dependency arrays
- [ ] Cleanup in useEffect return functions
- [ ] Stable callback references with useCallback
- [ ] Logic extracted to custom hooks when reusable
### Styling
- [ ] No styles attributes (use className with Tailwind)
- [ ] Dark mode variants only when values differ from light mode
- [ ] No duplicate dark: classes with identical values
- [ ] Consistent spacing using design tokens
- [ ] cn() for conditional classes
### Accessibility
- [ ] Semantic HTML elements
- [ ] ARIA labels and roles where needed
- [ ] Keyboard navigation support
- [ ] Focus management
### State Management
- [ ] Local state for UI-only concerns
- [ ] Zustand for shared/persisted state
- [ ] No duplicate state
- [ ] Clear state update patterns
### Error Handling
- [ ] try/catch around async operations
- [ ] Error states tracked and displayed
- [ ] Loading states for async actions
- [ ] Failures logged with context
---
## Anti-Patterns to Avoid
### ❌ Don't Do This
```typescript
// ❌ Inline styles
<div style={{ width: 200, marginTop: 10 }}>
// ❌ Duplicate dark mode classes (same value as light mode)
<div className='text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
<div className='bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'>
<div className='hover:bg-[var(--border)] dark:hover:bg-[var(--border)]'>
// ❌ console.log
console.log('Debug info')
// ❌ any type
const handleClick = (e: any) => {}
// ❌ Missing dependencies
useEffect(() => {
doSomething(prop)
}, []) // Missing prop!
// ❌ Mutating state
const handleAdd = () => {
items.push(newItem) // Mutating!
setItems(items)
}
// ❌ No error handling
const fetchData = async () => {
const data = await fetch('/api/data')
setData(data)
}
// ❌ Complex logic in component
export function Component() {
const [data, setData] = useState([])
useEffect(() => {
// 50 lines of complex logic
}, [])
}
```
### ✅ Do This Instead
```typescript
// ✅ Tailwind classes
<div className='w-[200px] mt-[10px]'>
// ✅ No duplicate dark classes - CSS variables already handle theming
<div className='text-[var(--text-primary)]'>
<div className='bg-[var(--surface-9)]'>
<div className='hover:bg-[var(--border)]'>
// ✅ Only add dark: when values differ between modes
<div className='bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'>
// ✅ Logger
logger.info('Debug info', { context })
// ✅ Proper types
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
// ✅ Complete dependencies
useEffect(() => {
doSomething(prop)
}, [prop])
// ✅ Immutable updates
const handleAdd = () => {
setItems([...items, newItem])
}
// ✅ Error handling
const fetchData = async () => {
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to fetch')
const data = await response.json()
setData(data)
} catch (error) {
logger.error('Fetch failed', { error })
setError(error)
}
}
// ✅ Extract to custom hook
export function Component() {
const { data, isLoading, error } = useFeatureData()
}
```
---
## Examples from Codebase
Study these files as reference implementations:
### Components
- `sidebar-new.tsx` - Main component structure, hook composition
- `workflow-list.tsx` - Complex component with drag-drop, memoization
- `blocks.tsx` - Simple panel component with resize
- `triggers.tsx` - Similar panel pattern
### Hooks
- `use-workspace-management.ts` - Complex hook with multiple operations, refs pattern
- `use-sidebar-resize.ts` - Simple focused hook with event listeners
- `use-workflow-operations.ts` - Hook coordinating store and navigation
- `use-panel-resize.ts` - Shared resize logic pattern
### Stores
- `stores/sidebar/store.ts` - Persist middleware, CSS variables, validation
---
## Summary
This architecture prioritizes:
1. **Separation of Concerns**: Components render, hooks contain logic, stores manage state
2. **Type Safety**: TypeScript everywhere with no escape hatches
3. **Performance**: Smart use of memoization and refs
4. **Maintainability**: Clear structure, documentation, and consistent patterns
5. **Developer Experience**: Logging, error handling, and clear interfaces
When in doubt, follow the patterns established in the sidebar-new component family.

View File

@@ -19,7 +19,7 @@ import {
ENTERPRISE_PLAN_FEATURES,
PRO_PLAN_FEATURES,
TEAM_PLAN_FEATURES,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/plan-configs'
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/plan-configs'
const logger = createLogger('LandingPricing')

View File

@@ -206,6 +206,9 @@
--terminal-status-info-bg: #f5f5f4; /* stone-100 */
--terminal-status-info-border: #a8a29e; /* stone-400 */
--terminal-status-info-color: #57534e; /* stone-600 */
--terminal-status-warning-bg: #fef9e7;
--terminal-status-warning-border: #f5c842;
--terminal-status-warning-color: #a16207;
}
.dark {
/* Neutrals (surfaces) */
@@ -336,6 +339,9 @@
--terminal-status-info-bg: #383838;
--terminal-status-info-border: #686868;
--terminal-status-info-color: #b7b7b7;
--terminal-status-warning-bg: #3d3520;
--terminal-status-warning-border: #5c4d1f;
--terminal-status-warning-color: #d4a72c;
}
}

View File

@@ -7,6 +7,42 @@ import { refreshOAuthToken } from '@/lib/oauth/oauth'
const logger = createLogger('OAuthUtilsAPI')
interface AccountInsertData {
id: string
userId: string
providerId: string
accountId: string
accessToken: string
scope: string
createdAt: Date
updatedAt: Date
refreshToken?: string
idToken?: string
}
/**
* Safely inserts an account record, handling duplicate constraint violations gracefully.
* If a duplicate is detected (unique constraint violation), logs a warning and returns success.
*/
export async function safeAccountInsert(
data: AccountInsertData,
context: { provider: string; identifier?: string }
): Promise<void> {
try {
await db.insert(account).values(data)
logger.info(`Created new ${context.provider} account for user`, { userId: data.userId })
} catch (error: any) {
if (error?.code === '23505') {
logger.error(`Duplicate ${context.provider} account detected, credential already exists`, {
userId: data.userId,
identifier: context.identifier,
})
} else {
throw error
}
}
}
/**
* Get the user ID based on either a session or a workflow ID
*/

View File

@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
const logger = createLogger('ShopifyStore')
@@ -66,14 +67,20 @@ export async function GET(request: NextRequest) {
await db.update(account).set(accountData).where(eq(account.id, existing.id))
logger.info('Updated existing Shopify account', { accountId: existing.id })
} else {
await db.insert(account).values({
id: `shopify_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'shopify',
...accountData,
createdAt: now,
})
logger.info('Created new Shopify account for user', { userId: session.user.id })
await safeAccountInsert(
{
id: `shopify_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'shopify',
accountId: accountData.accountId,
accessToken: accountData.accessToken,
scope: accountData.scope,
idToken: accountData.idToken,
createdAt: now,
updatedAt: now,
},
{ provider: 'Shopify', identifier: shopDomain }
)
}
const returnUrl = request.cookies.get('shopify_return_url')?.value

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { createLogger } from '@/lib/logs/console/logger'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
import { db } from '@/../../packages/db'
import { account } from '@/../../packages/db/schema'
@@ -67,16 +68,19 @@ export async function POST(request: NextRequest) {
})
.where(eq(account.id, existing.id))
} else {
await db.insert(account).values({
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope: 'read,write',
createdAt: now,
updatedAt: now,
})
await safeAccountInsert(
{
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope: 'read,write',
createdAt: now,
updatedAt: now,
},
{ provider: 'Trello', identifier: trelloUser.id }
)
}
return NextResponse.json({ success: true })

View File

@@ -4,7 +4,8 @@ import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails/render-email'
import { getRedisClient, markMessageAsProcessed, releaseLock } from '@/lib/core/config/redis'
import { getRedisClient } from '@/lib/core/config/redis'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -17,87 +18,80 @@ function generateOTP() {
return Math.floor(100000 + Math.random() * 900000).toString()
}
// OTP storage utility functions using Redis
// We use 15 minutes (900 seconds) expiry for OTPs
const OTP_EXPIRY = 15 * 60
const OTP_EXPIRY = 15 * 60 // 15 minutes
const OTP_EXPIRY_MS = OTP_EXPIRY * 1000
/**
* In-memory OTP storage for single-instance deployments without Redis.
* Only used when REDIS_URL is not configured (determined once at startup).
*
* Warning: This does NOT work in multi-instance/serverless deployments.
*/
const inMemoryOTPStore = new Map<string, { otp: string; expiresAt: number }>()
function cleanupExpiredOTPs() {
const now = Date.now()
for (const [key, value] of inMemoryOTPStore.entries()) {
if (value.expiresAt < now) {
inMemoryOTPStore.delete(key)
}
}
}
// Store OTP in Redis
async function storeOTP(email: string, chatId: string, otp: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
await redis.set(key, otp, 'EX', OTP_EXPIRY)
} else {
// Use the existing function as fallback to mark that an OTP exists
await markMessageAsProcessed(key, OTP_EXPIRY)
// For the fallback case, we need to handle storing the OTP value separately
// since markMessageAsProcessed only stores "1"
const valueKey = `${key}:value`
try {
// Access the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
if (inMemoryCache) {
const fullKey = `processed:${valueKey}`
const expiry = OTP_EXPIRY ? Date.now() + OTP_EXPIRY * 1000 : null
inMemoryCache.set(fullKey, { value: otp, expiry })
}
} catch (error) {
logger.error('Error storing OTP in fallback cache:', error)
}
cleanupExpiredOTPs()
inMemoryOTPStore.set(key, {
otp,
expiresAt: Date.now() + OTP_EXPIRY_MS,
})
}
}
// Get OTP from Redis
async function getOTP(email: string, chatId: string): Promise<string | null> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
return await redis.get(key)
}
// Use the existing function as fallback - check if it exists
const exists = await new Promise((resolve) => {
try {
// Check the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${key}`
const cacheEntry = inMemoryCache?.get(fullKey)
resolve(!!cacheEntry)
} catch {
resolve(false)
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
})
return redis.get(key)
}
if (!exists) return null
const entry = inMemoryOTPStore.get(key)
if (!entry) return null
// Try to get the value key
const valueKey = `${key}:value`
try {
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${valueKey}`
const cacheEntry = inMemoryCache?.get(fullKey)
return cacheEntry?.value || null
} catch {
if (entry.expiresAt < Date.now()) {
inMemoryOTPStore.delete(key)
return null
}
return entry.otp
}
// Delete OTP from Redis
async function deleteOTP(email: string, chatId: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
await redis.del(key)
} else {
// Use the existing function as fallback
await releaseLock(`processed:${key}`)
await releaseLock(`processed:${key}:value`)
inMemoryOTPStore.delete(key)
}
}
@@ -110,7 +104,6 @@ const otpVerifySchema = z.object({
otp: z.string().length(6, 'OTP must be 6 digits'),
})
// Send OTP endpoint
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
@@ -121,101 +114,82 @@ export async function POST(
try {
logger.debug(`[${requestId}] Processing OTP request for identifier: ${identifier}`)
// Parse request body
let body
try {
body = await request.json()
const { email } = otpRequestSchema.parse(body)
const body = await request.json()
const { email } = otpRequestSchema.parse(body)
// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
allowedEmails: chat.allowedEmails,
title: chat.title,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
allowedEmails: chat.allowedEmails,
title: chat.title,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
const deployment = deploymentResult[0]
// Verify this is an email-protected chat
if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
}
const allowedEmails: string[] = Array.isArray(deployment.allowedEmails)
? deployment.allowedEmails
: []
const isEmailAllowed =
allowedEmails.includes(email) ||
allowedEmails.some((allowed: string) => {
if (allowed.startsWith('@')) {
const domain = email.split('@')[1]
return domain && allowed === `@${domain}`
}
return false
})
if (!isEmailAllowed) {
return addCorsHeaders(
createErrorResponse('Email not authorized for this chat', 403),
request
)
}
const otp = generateOTP()
await storeOTP(email, deployment.id, otp)
const emailHtml = await renderOTPEmail(
otp,
email,
'email-verification',
deployment.title || 'Chat'
if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
}
const emailResult = await sendEmail({
to: email,
subject: `Verification code for ${deployment.title || 'Chat'}`,
html: emailHtml,
const allowedEmails: string[] = Array.isArray(deployment.allowedEmails)
? deployment.allowedEmails
: []
const isEmailAllowed =
allowedEmails.includes(email) ||
allowedEmails.some((allowed: string) => {
if (allowed.startsWith('@')) {
const domain = email.split('@')[1]
return domain && allowed === `@${domain}`
}
return false
})
if (!emailResult.success) {
logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message)
return addCorsHeaders(
createErrorResponse('Failed to send verification email', 500),
request
)
}
// Add a small delay to ensure Redis has fully processed the operation
// This helps with eventual consistency in distributed systems
await new Promise((resolve) => setTimeout(resolve, 500))
logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(createSuccessResponse({ message: 'Verification code sent' }), request)
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
if (!isEmailAllowed) {
return addCorsHeaders(createErrorResponse('Email not authorized for this chat', 403), request)
}
const otp = generateOTP()
await storeOTP(email, deployment.id, otp)
const emailHtml = await renderOTPEmail(
otp,
email,
'email-verification',
deployment.title || 'Chat'
)
const emailResult = await sendEmail({
to: email,
subject: `Verification code for ${deployment.title || 'Chat'}`,
html: emailHtml,
})
if (!emailResult.success) {
logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message)
return addCorsHeaders(createErrorResponse('Failed to send verification email', 500), request)
}
logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(createSuccessResponse({ message: 'Verification code sent' }), request)
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
logger.error(`[${requestId}] Error processing OTP request:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
@@ -224,7 +198,6 @@ export async function POST(
}
}
// Verify OTP endpoint
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
@@ -235,63 +208,50 @@ export async function PUT(
try {
logger.debug(`[${requestId}] Verifying OTP for identifier: ${identifier}`)
// Parse request body
let body
try {
body = await request.json()
const { email, otp } = otpVerifySchema.parse(body)
const body = await request.json()
const { email, otp } = otpVerifySchema.parse(body)
// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
// Check if OTP exists and is valid
const storedOTP = await getOTP(email, deployment.id)
if (!storedOTP) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
}
// Check if OTP matches
if (storedOTP !== otp) {
return addCorsHeaders(createErrorResponse('Invalid verification code', 400), request)
}
// OTP is valid, clean up
await deleteOTP(email, deployment.id)
// Create success response with auth cookie
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
// Set authentication cookie
setChatAuthCookie(response, deployment.id, deployment.authType)
return response
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
const storedOTP = await getOTP(email, deployment.id)
if (!storedOTP) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
}
if (storedOTP !== otp) {
return addCorsHeaders(createErrorResponse('Invalid verification code', 400), request)
}
await deleteOTP(email, deployment.id)
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
setChatAuthCookie(response, deployment.id, deployment.authType)
return response
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
logger.error(`[${requestId}] Error verifying OTP:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),

View File

@@ -151,8 +151,8 @@ export async function POST(
triggerType: 'chat',
executionId,
requestId,
checkRateLimit: false, // Chat bypasses rate limits
checkDeployment: true, // Chat requires deployed workflows
checkRateLimit: true,
checkDeployment: true,
loggingSession,
})

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, desc, eq, gte, inArray, lte, type SQL, sql } from 'drizzle-orm'
import { and, desc, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
@@ -126,13 +126,50 @@ export async function GET(request: NextRequest) {
// Build additional conditions for the query
let conditions: SQL | undefined
// Filter by level (supports comma-separated for OR conditions)
// Filter by level with support for derived statuses (running, pending)
if (params.level && params.level !== 'all') {
const levels = params.level.split(',').filter(Boolean)
if (levels.length === 1) {
conditions = and(conditions, eq(workflowExecutionLogs.level, levels[0]))
} else if (levels.length > 1) {
conditions = and(conditions, inArray(workflowExecutionLogs.level, levels))
const levelConditions: SQL[] = []
for (const level of levels) {
if (level === 'error') {
// Direct database field
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
} else if (level === 'info') {
// Completed info logs only (not running, not pending)
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
isNotNull(workflowExecutionLogs.endedAt)
)
if (condition) levelConditions.push(condition)
} else if (level === 'running') {
// Running logs: info level with no endedAt
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
isNull(workflowExecutionLogs.endedAt)
)
if (condition) levelConditions.push(condition)
} else if (level === 'pending') {
// Pending logs: info level with pause status indicators
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
or(
sql`(${pausedExecutions.totalPauseCount} > 0 AND ${pausedExecutions.resumedCount} < ${pausedExecutions.totalPauseCount})`,
and(
isNotNull(pausedExecutions.status),
sql`${pausedExecutions.status} != 'fully_resumed'`
)
)
)
if (condition) levelConditions.push(condition)
}
}
if (levelConditions.length > 0) {
conditions = and(
conditions,
levelConditions.length === 1 ? levelConditions[0] : or(...levelConditions)
)
}
}

View File

@@ -15,7 +15,6 @@ const logger = createLogger('McpToolExecutionAPI')
export const dynamic = 'force-dynamic'
// Type definitions for improved type safety
interface SchemaProperty {
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
description?: string
@@ -31,9 +30,6 @@ interface ToolExecutionResult {
error?: string
}
/**
* Type guard to safely check if a schema property has a type field
*/
function hasType(prop: unknown): prop is SchemaProperty {
return typeof prop === 'object' && prop !== null && 'type' in prop
}
@@ -57,7 +53,8 @@ export const POST = withMcpAuth('read')(
userId: userId,
})
const { serverId, toolName, arguments: args } = body
const { serverId, toolName, arguments: rawArgs } = body
const args = rawArgs || {}
const serverIdValidation = validateStringParam(serverId, 'serverId')
if (!serverIdValidation.isValid) {
@@ -75,22 +72,31 @@ export const POST = withMcpAuth('read')(
`[${requestId}] Executing tool ${toolName} on server ${serverId} for user ${userId} in workspace ${workspaceId}`
)
let tool = null
let tool: McpTool | null = null
try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
tool = tools.find((t) => t.name === toolName)
if (body.toolSchema) {
tool = {
name: toolName,
inputSchema: body.toolSchema,
serverId: serverId,
serverName: 'provided-schema',
} as McpTool
logger.debug(`[${requestId}] Using provided schema for ${toolName}, skipping discovery`)
} else {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
tool = tools.find((t) => t.name === toolName) ?? null
if (!tool) {
return createMcpErrorResponse(
new Error(
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
),
'Tool not found',
404
)
if (!tool) {
return createMcpErrorResponse(
new Error(
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
),
'Tool not found',
404
)
}
}
// Cast arguments to their expected types based on tool schema
if (tool.inputSchema?.properties) {
for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) {
const schema = paramSchema as any
@@ -100,7 +106,6 @@ export const POST = withMcpAuth('read')(
continue
}
// Cast numbers
if (
(schema.type === 'number' || schema.type === 'integer') &&
typeof value === 'string'
@@ -110,42 +115,33 @@ export const POST = withMcpAuth('read')(
if (!Number.isNaN(numValue)) {
args[paramName] = numValue
}
}
// Cast booleans
else if (schema.type === 'boolean' && typeof value === 'string') {
} else if (schema.type === 'boolean' && typeof value === 'string') {
if (value.toLowerCase() === 'true') {
args[paramName] = true
} else if (value.toLowerCase() === 'false') {
args[paramName] = false
}
}
// Cast arrays
else if (schema.type === 'array' && typeof value === 'string') {
} else if (schema.type === 'array' && typeof value === 'string') {
const stringValue = value.trim()
if (stringValue) {
try {
// Try to parse as JSON first (handles ["item1", "item2"])
const parsed = JSON.parse(stringValue)
if (Array.isArray(parsed)) {
args[paramName] = parsed
} else {
// JSON parsed but not an array, wrap in array
args[paramName] = [parsed]
}
} catch (error) {
// JSON parsing failed - treat as comma-separated if contains commas, otherwise single item
} catch {
if (stringValue.includes(',')) {
args[paramName] = stringValue
.split(',')
.map((item) => item.trim())
.filter((item) => item)
} else {
// Single item - wrap in array since schema expects array
args[paramName] = [stringValue]
}
}
} else {
// Empty string becomes empty array
args[paramName] = []
}
}
@@ -172,7 +168,7 @@ export const POST = withMcpAuth('read')(
const toolCall: McpToolCall = {
name: toolName,
arguments: args || {},
arguments: args,
}
const result = await Promise.race([
@@ -197,7 +193,6 @@ export const POST = withMcpAuth('read')(
}
logger.info(`[${requestId}] Successfully executed tool ${toolName} on server ${serverId}`)
// Track MCP tool execution
try {
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
trackPlatformEvent('platform.mcp.tool_executed', {
@@ -206,8 +201,8 @@ export const POST = withMcpAuth('read')(
'mcp.execution_status': 'success',
'workspace.id': workspaceId,
})
} catch (_e) {
// Silently fail
} catch {
// Telemetry failure is non-critical
}
return createMcpSuccessResponse(transformedResult)
@@ -220,12 +215,9 @@ export const POST = withMcpAuth('read')(
}
)
/**
* Validate tool arguments against schema
*/
function validateToolArguments(tool: McpTool, args: Record<string, unknown>): string | null {
if (!tool.inputSchema) {
return null // No schema to validate against
return null
}
const schema = tool.inputSchema
@@ -270,9 +262,6 @@ function validateToolArguments(tool: McpTool, args: Record<string, unknown>): st
return null
}
/**
* Transform MCP tool result to platform format
*/
function transformToolResult(result: McpToolResult): ToolExecutionResult {
if (result.isError) {
return {

View File

@@ -16,15 +16,17 @@ export async function GET(request: NextRequest) {
const requestId = nanoid()
logger.info(`Inactivity alert polling triggered (${requestId})`)
let lockAcquired = false
try {
const authError = verifyCronAuth(request, 'Inactivity alert polling')
if (authError) {
return authError
}
const locked = await acquireLock(LOCK_KEY, requestId, LOCK_TTL_SECONDS)
lockAcquired = await acquireLock(LOCK_KEY, requestId, LOCK_TTL_SECONDS)
if (!locked) {
if (!lockAcquired) {
return NextResponse.json(
{
success: true,
@@ -57,6 +59,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockAcquired) {
await releaseLock(LOCK_KEY, requestId).catch(() => {})
}
}
}

View File

@@ -18,7 +18,6 @@ export async function GET(request: NextRequest) {
}
const authenticatedUserId = auth.userId
// Rate limit info (sync + async), mirroring /users/me/rate-limit
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
const rateLimiter = new RateLimiter()
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
@@ -37,7 +36,6 @@ export async function GET(request: NextRequest) {
),
])
// Usage summary (current period cost + limit + plan)
const [usageCheck, effectiveCost, storageUsage, storageLimit] = await Promise.all([
checkServerSideUsageLimits(authenticatedUserId),
getEffectiveCurrentPeriodCost(authenticatedUserId),
@@ -52,13 +50,15 @@ export async function GET(request: NextRequest) {
rateLimit: {
sync: {
isLimited: syncStatus.remaining === 0,
limit: syncStatus.limit,
requestsPerMinute: syncStatus.requestsPerMinute,
maxBurst: syncStatus.maxBurst,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt,
},
async: {
isLimited: asyncStatus.remaining === 0,
limit: asyncStatus.limit,
requestsPerMinute: asyncStatus.requestsPerMinute,
maxBurst: asyncStatus.maxBurst,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt,
},

View File

@@ -6,12 +6,14 @@ import { RateLimiter } from '@/lib/core/rate-limiter'
export interface UserLimits {
workflowExecutionRateLimit: {
sync: {
limit: number
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
async: {
limit: number
requestsPerMinute: number
maxBurst: number
remaining: number
resetAt: string
}
@@ -40,12 +42,14 @@ export async function getUserLimits(userId: string): Promise<UserLimits> {
return {
workflowExecutionRateLimit: {
sync: {
limit: syncStatus.limit,
requestsPerMinute: syncStatus.requestsPerMinute,
maxBurst: syncStatus.maxBurst,
remaining: syncStatus.remaining,
resetAt: syncStatus.resetAt.toISOString(),
},
async: {
limit: asyncStatus.limit,
requestsPerMinute: asyncStatus.requestsPerMinute,
maxBurst: asyncStatus.maxBurst,
remaining: asyncStatus.remaining,
resetAt: asyncStatus.resetAt.toISOString(),
},

View File

@@ -1,6 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { createLogger } from '@/lib/logs/console/logger'
import { authenticateV1Request } from '@/app/api/v1/auth'
@@ -12,6 +12,7 @@ export interface RateLimitResult {
remaining: number
resetAt: Date
limit: number
retryAfterMs?: number
userId?: string
error?: string
}
@@ -26,7 +27,7 @@ export async function checkRateLimit(
return {
allowed: false,
remaining: 0,
limit: 10, // Default to free tier limit
limit: 10,
resetAt: new Date(),
error: auth.error,
}
@@ -35,12 +36,11 @@ export async function checkRateLimit(
const userId = auth.userId!
const subscription = await getHighestPrioritySubscription(userId)
// Use api-endpoint trigger type for external API rate limiting
const result = await rateLimiter.checkRateLimitWithSubscription(
userId,
subscription,
'api-endpoint',
false // Not relevant for api-endpoint trigger type
false
)
if (!result.allowed) {
@@ -51,7 +51,6 @@ export async function checkRateLimit(
})
}
// Get the actual rate limit for this user's plan
const rateLimitStatus = await rateLimiter.getRateLimitStatusWithSubscription(
userId,
subscription,
@@ -60,8 +59,11 @@ export async function checkRateLimit(
)
return {
...result,
limit: rateLimitStatus.limit,
allowed: result.allowed,
remaining: result.remaining,
resetAt: result.resetAt,
limit: rateLimitStatus.requestsPerMinute,
retryAfterMs: result.retryAfterMs,
userId,
}
} catch (error) {
@@ -88,6 +90,10 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
}
if (!result.allowed) {
const retryAfterSeconds = result.retryAfterMs
? Math.ceil(result.retryAfterMs / 1000)
: Math.ceil((result.resetAt.getTime() - Date.now()) / 1000)
return NextResponse.json(
{
error: 'Rate limit exceeded',
@@ -98,7 +104,7 @@ export function createRateLimitResponse(result: RateLimitResult): NextResponse {
status: 429,
headers: {
...headers,
'Retry-After': Math.ceil((result.resetAt.getTime() - Date.now()) / 1000).toString(),
'Retry-After': retryAfterSeconds.toString(),
},
}
)

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, gte, inArray, lte } from 'drizzle-orm'
import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
@@ -15,6 +15,7 @@ const QueryParamsSchema = z.object({
workflowIds: z.string().optional(),
folderIds: z.string().optional(),
triggers: z.string().optional(),
level: z.string().optional(), // Supports comma-separated values: 'error,running'
})
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -84,20 +85,73 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
inArray(workflowExecutionLogs.workflowId, workflowIdList),
gte(workflowExecutionLogs.startedAt, start),
lte(workflowExecutionLogs.startedAt, end),
] as any[]
] as SQL[]
if (qp.triggers) {
const t = qp.triggers.split(',').filter(Boolean)
logWhere.push(inArray(workflowExecutionLogs.trigger, t))
}
// Handle level filtering with support for derived statuses and multiple selections
if (qp.level && qp.level !== 'all') {
const levels = qp.level.split(',').filter(Boolean)
const levelConditions: SQL[] = []
for (const level of levels) {
if (level === 'error') {
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
} else if (level === 'info') {
// Completed info logs only
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
isNotNull(workflowExecutionLogs.endedAt)
)
if (condition) levelConditions.push(condition)
} else if (level === 'running') {
// Running logs: info level with no endedAt
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
isNull(workflowExecutionLogs.endedAt)
)
if (condition) levelConditions.push(condition)
} else if (level === 'pending') {
// Pending logs: info level with pause status indicators
const condition = and(
eq(workflowExecutionLogs.level, 'info'),
or(
sql`(${pausedExecutions.totalPauseCount} > 0 AND ${pausedExecutions.resumedCount} < ${pausedExecutions.totalPauseCount})`,
and(
isNotNull(pausedExecutions.status),
sql`${pausedExecutions.status} != 'fully_resumed'`
)
)
)
if (condition) levelConditions.push(condition)
}
}
if (levelConditions.length > 0) {
const combinedCondition =
levelConditions.length === 1 ? levelConditions[0] : or(...levelConditions)
if (combinedCondition) logWhere.push(combinedCondition)
}
}
const logs = await db
.select({
workflowId: workflowExecutionLogs.workflowId,
level: workflowExecutionLogs.level,
startedAt: workflowExecutionLogs.startedAt,
endedAt: workflowExecutionLogs.endedAt,
totalDurationMs: workflowExecutionLogs.totalDurationMs,
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
pausedResumedCount: pausedExecutions.resumedCount,
pausedStatus: pausedExecutions.status,
})
.from(workflowExecutionLogs)
.leftJoin(
pausedExecutions,
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
)
.where(and(...logWhere))
type Bucket = {

View File

@@ -0,0 +1,182 @@
'use client'
import { useState } from 'react'
import { ArrowDown, Download, Loader2, Music } from 'lucide-react'
import { Button } from '@/components/emcn'
import { DefaultFileIcon, getDocumentIcon } from '@/components/icons/document-icons'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChatFile } from '@/app/chat/components/message/message'
const logger = createLogger('ChatFileDownload')
interface ChatFileDownloadProps {
file: ChatFile
}
interface ChatFileDownloadAllProps {
files: ChatFile[]
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${Math.round((bytes / k ** i) * 10) / 10} ${sizes[i]}`
}
function isAudioFile(mimeType: string, filename: string): boolean {
const audioMimeTypes = [
'audio/mpeg',
'audio/wav',
'audio/mp3',
'audio/ogg',
'audio/webm',
'audio/aac',
'audio/flac',
]
const audioExtensions = ['mp3', 'wav', 'ogg', 'webm', 'aac', 'flac', 'm4a']
const extension = filename.split('.').pop()?.toLowerCase()
return (
audioMimeTypes.some((t) => mimeType.includes(t)) ||
(extension ? audioExtensions.includes(extension) : false)
)
}
function isImageFile(mimeType: string): boolean {
return mimeType.startsWith('image/')
}
function getFileUrl(file: ChatFile): string {
return `/api/files/serve/${encodeURIComponent(file.key)}?context=${file.context || 'execution'}`
}
async function triggerDownload(url: string, filename: string): Promise<void> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(blobUrl)
logger.info(`Downloaded: ${filename}`)
}
export function ChatFileDownload({ file }: ChatFileDownloadProps) {
const [isDownloading, setIsDownloading] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const handleDownload = async () => {
if (isDownloading) return
setIsDownloading(true)
try {
logger.info(`Initiating download for file: ${file.name}`)
const url = getFileUrl(file)
await triggerDownload(url, file.name)
} catch (error) {
logger.error(`Failed to download file ${file.name}:`, error)
if (file.url) {
window.open(file.url, '_blank')
}
} finally {
setIsDownloading(false)
}
}
const renderIcon = () => {
if (isAudioFile(file.type, file.name)) {
return <Music className='h-4 w-4 text-purple-500' />
}
if (isImageFile(file.type)) {
const ImageIcon = DefaultFileIcon
return <ImageIcon className='h-5 w-5' />
}
const DocumentIcon = getDocumentIcon(file.type, file.name)
return <DocumentIcon className='h-5 w-5' />
}
return (
<Button
variant='default'
onClick={handleDownload}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
disabled={isDownloading}
className='flex h-auto w-[200px] items-center gap-2 rounded-lg px-3 py-2'
>
<div className='flex h-8 w-8 flex-shrink-0 items-center justify-center'>{renderIcon()}</div>
<div className='min-w-0 flex-1 text-left'>
<div className='w-[100px] truncate text-xs'>{file.name}</div>
<div className='text-[10px] text-[var(--text-muted)]'>{formatFileSize(file.size)}</div>
</div>
<div className='flex-shrink-0'>
{isDownloading ? (
<Loader2 className='h-3.5 w-3.5 animate-spin' />
) : (
<ArrowDown
className={`h-3.5 w-3.5 transition-opacity ${isHovered ? 'opacity-100' : 'opacity-0'}`}
/>
)}
</div>
</Button>
)
}
export function ChatFileDownloadAll({ files }: ChatFileDownloadAllProps) {
const [isDownloading, setIsDownloading] = useState(false)
if (!files || files.length === 0) return null
const handleDownloadAll = async () => {
if (isDownloading) return
setIsDownloading(true)
try {
logger.info(`Initiating download for ${files.length} files`)
for (let i = 0; i < files.length; i++) {
const file = files[i]
try {
const url = getFileUrl(file)
await triggerDownload(url, file.name)
logger.info(`Downloaded file ${i + 1}/${files.length}: ${file.name}`)
if (i < files.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 150))
}
} catch (error) {
logger.error(`Failed to download file ${file.name}:`, error)
}
}
} finally {
setIsDownloading(false)
}
}
return (
<button
onClick={handleDownloadAll}
disabled={isDownloading}
className='text-muted-foreground transition-colors hover:bg-muted disabled:opacity-50'
>
{isDownloading ? (
<Loader2 className='h-3 w-3 animate-spin' strokeWidth={2} />
) : (
<Download className='h-3 w-3' strokeWidth={2} />
)}
</button>
)
}

View File

@@ -3,6 +3,10 @@
import { memo, useMemo, useState } from 'react'
import { Check, Copy, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import {
ChatFileDownload,
ChatFileDownloadAll,
} from '@/app/chat/components/message/components/file-download'
import MarkdownRenderer from '@/app/chat/components/message/components/markdown-renderer'
export interface ChatAttachment {
@@ -13,6 +17,16 @@ export interface ChatAttachment {
size?: number
}
export interface ChatFile {
id: string
name: string
url: string
key: string
size: number
type: string
context?: string
}
export interface ChatMessage {
id: string
content: string | Record<string, unknown>
@@ -21,6 +35,7 @@ export interface ChatMessage {
isInitialMessage?: boolean
isStreaming?: boolean
attachments?: ChatAttachment[]
files?: ChatFile[]
}
function EnhancedMarkdownRenderer({ content }: { content: string }) {
@@ -177,6 +192,13 @@ export const ClientChatMessage = memo(
)}
</div>
</div>
{message.files && message.files.length > 0 && (
<div className='flex flex-wrap gap-2'>
{message.files.map((file) => (
<ChatFileDownload key={file.id} file={file} />
))}
</div>
)}
{message.type === 'assistant' && !isJsonObject && !message.isInitialMessage && (
<div className='flex items-center justify-start space-x-2'>
{/* Copy Button - Only show when not streaming */}
@@ -207,6 +229,10 @@ export const ClientChatMessage = memo(
</Tooltip.Content>
</Tooltip.Root>
)}
{/* Download All Button - Only show when there are files */}
{!message.isStreaming && message.files && (
<ChatFileDownloadAll files={message.files} />
)}
</div>
)}
</div>
@@ -221,7 +247,8 @@ export const ClientChatMessage = memo(
prevProps.message.id === nextProps.message.id &&
prevProps.message.content === nextProps.message.content &&
prevProps.message.isStreaming === nextProps.message.isStreaming &&
prevProps.message.isInitialMessage === nextProps.message.isInitialMessage
prevProps.message.isInitialMessage === nextProps.message.isInitialMessage &&
prevProps.message.files?.length === nextProps.message.files?.length
)
}
)

View File

@@ -1,12 +1,52 @@
'use client'
import { useRef, useState } from 'react'
import { isUserFile } from '@/lib/core/utils/display-filters'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChatMessage } from '@/app/chat/components/message/message'
import type { ChatFile, ChatMessage } from '@/app/chat/components/message/message'
import { CHAT_ERROR_MESSAGES } from '@/app/chat/constants'
const logger = createLogger('UseChatStreaming')
function extractFilesFromData(
data: any,
files: ChatFile[] = [],
seenIds = new Set<string>()
): ChatFile[] {
if (!data || typeof data !== 'object') {
return files
}
if (isUserFile(data)) {
if (!seenIds.has(data.id)) {
seenIds.add(data.id)
files.push({
id: data.id,
name: data.name,
url: data.url,
key: data.key,
size: data.size,
type: data.type,
context: data.context,
})
}
return files
}
if (Array.isArray(data)) {
for (const item of data) {
extractFilesFromData(item, files, seenIds)
}
return files
}
for (const value of Object.values(data)) {
extractFilesFromData(value, files, seenIds)
}
return files
}
export interface VoiceSettings {
isVoiceEnabled: boolean
voiceId: string
@@ -185,12 +225,21 @@ export function useChatStreaming() {
const outputConfigs = streamingOptions?.outputConfigs
const formattedOutputs: string[] = []
let extractedFiles: ChatFile[] = []
const formatValue = (value: any): string | null => {
if (value === null || value === undefined) {
return null
}
if (isUserFile(value)) {
return null
}
if (Array.isArray(value) && value.length === 0) {
return null
}
if (typeof value === 'string') {
return value
}
@@ -235,6 +284,26 @@ export function useChatStreaming() {
if (!blockOutputs) continue
const value = getOutputValue(blockOutputs, config.path)
if (isUserFile(value)) {
extractedFiles.push({
id: value.id,
name: value.name,
url: value.url,
key: value.key,
size: value.size,
type: value.type,
context: value.context,
})
continue
}
const nestedFiles = extractFilesFromData(value)
if (nestedFiles.length > 0) {
extractedFiles = [...extractedFiles, ...nestedFiles]
continue
}
const formatted = formatValue(value)
if (formatted) {
formattedOutputs.push(formatted)
@@ -267,7 +336,7 @@ export function useChatStreaming() {
}
}
if (!finalContent) {
if (!finalContent && extractedFiles.length === 0) {
if (finalData.error) {
if (typeof finalData.error === 'string') {
finalContent = finalData.error
@@ -291,6 +360,7 @@ export function useChatStreaming() {
...msg,
isStreaming: false,
content: finalContent ?? msg.content,
files: extractedFiles.length > 0 ? extractedFiles : undefined,
}
: msg
)

View File

@@ -4,11 +4,41 @@ import { useEffect, useState } from 'react'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { client, useSession } from '@/lib/auth/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getErrorMessage } from '@/app/invite/[id]/utils'
import { InviteLayout, InviteStatusCard } from '@/app/invite/components'
const logger = createLogger('InviteById')
function getErrorMessage(reason: string): string {
switch (reason) {
case 'missing-token':
return 'The invitation link is invalid or missing a required parameter.'
case 'invalid-token':
return 'The invitation link is invalid or has already been used.'
case 'expired':
return 'This invitation has expired. Please ask for a new invitation.'
case 'already-processed':
return 'This invitation has already been accepted or declined.'
case 'email-mismatch':
return 'This invitation was sent to a different email address. Please log in with the correct account.'
case 'workspace-not-found':
return 'The workspace associated with this invitation could not be found.'
case 'user-not-found':
return 'Your user account could not be found. Please try logging out and logging back in.'
case 'already-member':
return 'You are already a member of this organization or workspace.'
case 'already-in-organization':
return 'You are already a member of an organization. Leave your current organization before accepting a new invitation.'
case 'invalid-invitation':
return 'This invitation is invalid or no longer exists.'
case 'missing-invitation-id':
return 'The invitation link is missing required information. Please use the original invitation link.'
case 'server-error':
return 'An unexpected error occurred while processing your invitation. Please try again later.'
default:
return 'An unknown error occurred while processing your invitation.'
}
}
export default function Invite() {
const router = useRouter()
const params = useParams()

View File

@@ -1,30 +0,0 @@
export function getErrorMessage(reason: string): string {
switch (reason) {
case 'missing-token':
return 'The invitation link is invalid or missing a required parameter.'
case 'invalid-token':
return 'The invitation link is invalid or has already been used.'
case 'expired':
return 'This invitation has expired. Please ask for a new invitation.'
case 'already-processed':
return 'This invitation has already been accepted or declined.'
case 'email-mismatch':
return 'This invitation was sent to a different email address. Please log in with the correct account.'
case 'workspace-not-found':
return 'The workspace associated with this invitation could not be found.'
case 'user-not-found':
return 'Your user account could not be found. Please try logging out and logging back in.'
case 'already-member':
return 'You are already a member of this organization or workspace.'
case 'already-in-organization':
return 'You are already a member of an organization. Leave your current organization before accepting a new invitation.'
case 'invalid-invitation':
return 'This invitation is invalid or no longer exists.'
case 'missing-invitation-id':
return 'The invitation link is missing required information. Please use the original invitation link.'
case 'server-error':
return 'An unexpected error occurred while processing your invitation. Please try again later.'
default:
return 'An unknown error occurred while processing your invitation.'
}
}

View File

@@ -3,7 +3,6 @@
import { useEffect, useState } from 'react'
import { formatDistanceToNow } from 'date-fns'
import {
ArrowLeft,
ChartNoAxesColumn,
ChevronDown,
Globe,
@@ -16,6 +15,7 @@ import {
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import {
Breadcrumb,
Button,
Copy,
Popover,
@@ -267,13 +267,13 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
}
}
const handleBack = () => {
if (isWorkspaceContext) {
router.back()
} else {
router.push('/templates')
}
}
const breadcrumbItems = [
{
label: 'Templates',
href: isWorkspaceContext ? `/workspace/${workspaceId}/templates` : '/templates',
},
{ label: template?.name || 'Template' },
]
/**
* Intercepts wheel events over the workflow preview so that the page handles scrolling
* instead of the underlying canvas. We stop propagation in the capture phase to prevent
@@ -542,24 +542,15 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
}
return (
<div className={cn('flex min-h-screen flex-col', isWorkspaceContext && 'pl-64')}>
<div className={cn('flex flex-col', isWorkspaceContext ? 'h-full flex-1' : 'min-h-screen')}>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
{/* Top bar with back button */}
<div className='flex items-center justify-between'>
{/* Back button */}
<button
onClick={handleBack}
className='flex items-center gap-[6px] font-medium text-[#ADADAD] text-[14px] transition-colors hover:text-white'
>
<ArrowLeft className='h-[14px] w-[14px]' />
<span>More Templates</span>
</button>
</div>
{/* Breadcrumb navigation */}
<Breadcrumb items={breadcrumbItems} />
{/* Template name and action buttons */}
<div className='mt-[24px] flex items-center justify-between'>
<h1 className='font-medium text-[18px]'>{template.name}</h1>
<div className='mt-[14px] flex items-center justify-between'>
<h1 className='font-medium text-[18px] text-[var(--text-primary)]'>{template.name}</h1>
{/* Action buttons */}
<div className='flex items-center gap-[8px]'>
@@ -706,7 +697,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Template tagline */}
{template.details?.tagline && (
<p className='mt-[4px] font-medium text-[#888888] text-[14px]'>
<p className='mt-[4px] font-medium text-[14px] text-[var(--text-tertiary)]'>
{template.details.tagline}
</p>
)}
@@ -718,18 +709,22 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
onClick={handleStarToggle}
className={cn(
'h-[14px] w-[14px] cursor-pointer transition-colors',
template.isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
template.isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-muted)]',
starTemplate.isPending && 'opacity-50'
)}
/>
<span className='font-medium text-[#888888] text-[14px]'>{template.stars || 0}</span>
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.stars || 0}
</span>
{/* Users icon and count */}
<ChartNoAxesColumn className='h-[16px] w-[16px] text-[#888888]' />
<span className='font-medium text-[#888888] text-[14px]'>{template.views}</span>
<ChartNoAxesColumn className='h-[16px] w-[16px] text-[var(--text-muted)]' />
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.views}
</span>
{/* Vertical divider */}
<div className='mx-[4px] mb-[-1.5px] h-[18px] w-[1.25px] rounded-full bg-[#3A3A3A]' />
<div className='mx-[4px] mb-[-1.5px] h-[18px] w-[1.25px] rounded-full bg-[var(--border)]' />
{/* Creator profile pic */}
{template.creator?.profileImageUrl ? (
@@ -741,13 +736,13 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/>
</div>
) : (
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[#4A4A4A]'>
<User className='h-[14px] w-[14px] text-[#888888]' />
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<User className='h-[14px] w-[14px] text-[var(--text-muted)]' />
</div>
)}
{/* Creator name */}
<div className='flex items-center gap-[4px]'>
<span className='font-medium text-[#8B8B8B] text-[14px]'>
<span className='font-medium text-[14px] text-[var(--text-muted)]'>
{template.creator?.name || 'Unknown'}
</span>
{template.creator?.verified && <VerifiedBadge size='md' />}
@@ -757,7 +752,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Credentials needed */}
{Array.isArray(template.requiredCredentials) &&
template.requiredCredentials.length > 0 && (
<p className='mt-[12px] font-medium text-[#888888] text-[12px]'>
<p className='mt-[12px] font-medium text-[12px] text-[var(--text-muted)]'>
Credentials needed:{' '}
{template.requiredCredentials
.map((cred: CredentialRequirement) => {
@@ -783,7 +778,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Last updated overlay */}
{template.updatedAt && (
<div className='pointer-events-none absolute right-[12px] bottom-[12px] rounded-[4px] bg-[var(--bg)]/80 px-[8px] py-[4px] backdrop-blur-sm'>
<span className='font-medium text-[#8B8B8B] text-[12px]'>
<span className='font-medium text-[12px] text-[var(--text-muted)]'>
Last updated{' '}
{formatDistanceToNow(new Date(template.updatedAt), {
addSuffix: true,
@@ -910,8 +905,8 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/>
</div>
) : (
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[#4A4A4A]'>
<User className='h-[24px] w-[24px] text-[#888888]' />
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<User className='h-[24px] w-[24px] text-[var(--text-muted)]' />
</div>
)}
@@ -932,7 +927,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
href={template.creator.details.websiteUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[#888888] transition-colors hover:text-[var(--text-primary)]'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='Website'
>
<Globe className='h-[14px] w-[14px]' />
@@ -943,7 +938,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
href={template.creator.details.xUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[#888888] transition-colors hover:text-[var(--text-primary)]'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='X (Twitter)'
>
<svg
@@ -960,7 +955,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
href={template.creator.details.linkedinUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center text-[#888888] transition-colors hover:text-[var(--text-primary)]'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='LinkedIn'
>
<Linkedin className='h-[14px] w-[14px]' />
@@ -969,7 +964,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{template.creator.details?.contactEmail && (
<a
href={`mailto:${template.creator.details.contactEmail}`}
className='flex items-center text-[#888888] transition-colors hover:text-[var(--text-primary)]'
className='flex items-center text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)]'
aria-label='Email'
>
<Mail className='h-[14px] w-[14px]' />

View File

@@ -199,7 +199,7 @@ function TemplateCardInner({
>
<div
ref={previewRef}
className='pointer-events-none h-[180px] w-full overflow-hidden rounded-[6px]'
className='pointer-events-none h-[180px] w-full cursor-pointer overflow-hidden rounded-[6px]'
>
{normalizedState && isInView ? (
<WorkflowPreview

View File

@@ -135,15 +135,15 @@ export default function Templates({
return (
<div className='flex h-[100vh] flex-col'>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[28px] pb-[24px]'>
<div>
<div className='flex items-start gap-[12px]'>
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#7A5F11] bg-[#514215]'>
<Layout className='h-[14px] w-[14px] text-[#FBBC04]' />
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#1E3A5A] bg-[#0F2A3D]'>
<Layout className='h-[14px] w-[14px] text-[#60A5FA]' />
</div>
<h1 className='font-medium text-[18px]'>Templates</h1>
</div>
<p className='mt-[10px] font-base text-[#888888] text-[14px]'>
<p className='mt-[10px] text-[14px] text-[var(--text-tertiary)]'>
Grab a template and start building, or make one from scratch.
</p>
</div>
@@ -178,15 +178,13 @@ export default function Templates({
</div>
</div>
<div className='mt-[24px] h-[1px] w-full border-[var(--border)] border-t' />
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{loading ? (
Array.from({ length: 8 }).map((_, index) => (
<TemplateCardSkeleton key={`skeleton-${index}`} />
))
) : filteredTemplates.length === 0 ? (
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 border-dashed bg-muted/20'>
<div className='col-span-full flex h-64 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
<div className='text-center'>
<p className='font-medium text-muted-foreground text-sm'>{emptyState.title}</p>
<p className='mt-1 text-muted-foreground/70 text-xs'>{emptyState.description}</p>

View File

@@ -2,15 +2,16 @@
import { useRef, useState } from 'react'
import { AlertCircle, Loader2 } from 'lucide-react'
import { Button, Textarea } from '@/components/emcn'
import {
Button,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/emcn/components/modal/modal'
import { Label } from '@/components/ui/label'
Textarea,
} from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
@@ -119,24 +120,12 @@ export function CreateChunkModal({
return (
<>
<Modal open={open} onOpenChange={handleCloseAttempt}>
<ModalContent className='h-[74vh] sm:max-w-[600px]'>
<ModalContent size='lg'>
<ModalHeader>Create Chunk</ModalHeader>
<form className='flex min-h-0 flex-1 flex-col'>
<ModalBody>
<div className='space-y-[12px]'>
{/* Document Info Section */}
<div className='flex items-center gap-3 rounded-lg border p-4'>
<div className='min-w-0 flex-1'>
<p className='font-medium text-[var(--text-primary)] text-sm'>
{document?.filename || 'Unknown Document'}
</p>
<p className='text-[var(--text-tertiary)] text-xs'>
Adding chunk to this document
</p>
</div>
</div>
<form>
<ModalBody className='!pb-[16px]'>
<div className='flex flex-col gap-[8px]'>
{/* Error Display */}
{error && (
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
@@ -146,22 +135,15 @@ export function CreateChunkModal({
)}
{/* Content Input Section */}
<div className='space-y-[8px]'>
<Label
htmlFor='content'
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
>
Chunk Content
</Label>
<Textarea
id='content'
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder='Enter the content for this chunk...'
rows={10}
disabled={isCreating}
/>
</div>
<Label htmlFor='content'>Chunk</Label>
<Textarea
id='content'
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder='Enter the content for this chunk...'
rows={12}
disabled={isCreating}
/>
</div>
</ModalBody>
@@ -196,7 +178,7 @@ export function CreateChunkModal({
{/* Unsaved Changes Alert */}
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Discard Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>

View File

@@ -1,9 +1,7 @@
'use client'
import { useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChunkData } from '@/stores/knowledge/store'
@@ -68,7 +66,7 @@ export function DeleteChunkModal({
return (
<Modal open={isOpen} onOpenChange={onClose}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Delete Chunk</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
@@ -86,17 +84,7 @@ export function DeleteChunkModal({
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeleting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Deleting...
</>
) : (
<>
<Trash className='mr-2 h-4 w-4' />
Delete
</>
)}
{isDeleting ? <>Deleting...</> : <>Delete</>}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -1,80 +0,0 @@
'use client'
import { Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import {
ChunkTableSkeleton,
KnowledgeHeader,
} from '@/app/workspace/[workspaceId]/knowledge/components'
interface DocumentLoadingProps {
knowledgeBaseId: string
knowledgeBaseName: string
documentName: string
}
export function DocumentLoading({
knowledgeBaseId,
knowledgeBaseName,
documentName,
}: DocumentLoadingProps) {
const params = useParams()
const workspaceId = params?.workspaceId as string
const breadcrumbs = [
{
id: 'knowledge-root',
label: 'Knowledge',
href: `/workspace/${workspaceId}/knowledge`,
},
{
id: `knowledge-base-${knowledgeBaseId}`,
label: knowledgeBaseName,
href: `/workspace/${workspaceId}/knowledge/${knowledgeBaseId}`,
},
{
id: `document-${knowledgeBaseId}-${documentName}`,
label: documentName,
},
]
return (
<div className='flex h-[100vh] flex-col pl-64'>
{/* Header with Breadcrumbs */}
<KnowledgeHeader breadcrumbs={breadcrumbs} />
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-hidden'>
{/* Main Content */}
<div className='flex-1 overflow-auto'>
<div className='px-6 pb-6'>
{/* Search Section */}
<div className='mb-4 flex items-center justify-between pt-1'>
<div className='relative max-w-md'>
<div className='relative flex items-center'>
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-[18px] w-[18px] transform text-muted-foreground' />
<input
type='text'
placeholder='Search chunks...'
disabled
className='h-10 w-full rounded-md border bg-background px-9 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'
/>
</div>
</div>
<Button disabled variant='primary' className='flex items-center gap-1'>
<div className='h-3.5 w-3.5 animate-pulse rounded bg-primary-foreground/30' />
<span>Create Chunk</span>
</Button>
</div>
{/* Table container */}
<ChunkTableSkeleton isSidebarCollapsed={false} rowCount={8} />
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,649 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import {
Button,
Combobox,
Input,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Trash,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { MAX_TAG_SLOTS, TAG_SLOTS, type TagSlot } from '@/lib/knowledge/constants'
import type { DocumentTag } from '@/lib/knowledge/tags/types'
import { createLogger } from '@/lib/logs/console/logger'
import {
type TagDefinition,
useKnowledgeBaseTagDefinitions,
} from '@/hooks/use-knowledge-base-tag-definitions'
import { useNextAvailableSlot } from '@/hooks/use-next-available-slot'
import { type TagDefinitionInput, useTagDefinitions } from '@/hooks/use-tag-definitions'
import { type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'
const logger = createLogger('DocumentTagsModal')
interface DocumentTagsModalProps {
open: boolean
onOpenChange: (open: boolean) => void
knowledgeBaseId: string
documentId: string
documentData: DocumentData | null
onDocumentUpdate?: (updates: Record<string, string>) => void
}
export function DocumentTagsModal({
open,
onOpenChange,
knowledgeBaseId,
documentId,
documentData,
onDocumentUpdate,
}: DocumentTagsModalProps) {
const { updateDocument: updateDocumentInStore } = useKnowledgeStore()
const documentTagHook = useTagDefinitions(knowledgeBaseId, documentId)
const kbTagHook = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
const { getNextAvailableSlot: getServerNextSlot } = useNextAvailableSlot(knowledgeBaseId)
const { saveTagDefinitions, tagDefinitions, fetchTagDefinitions } = documentTagHook
const { tagDefinitions: kbTagDefinitions, fetchTagDefinitions: refreshTagDefinitions } = kbTagHook
const [documentTags, setDocumentTags] = useState<DocumentTag[]>([])
const [editingTagIndex, setEditingTagIndex] = useState<number | null>(null)
const [isCreatingTag, setIsCreatingTag] = useState(false)
const [isSavingTag, setIsSavingTag] = useState(false)
const [editTagForm, setEditTagForm] = useState({
displayName: '',
fieldType: 'text',
value: '',
})
const buildDocumentTags = useCallback((docData: DocumentData, definitions: TagDefinition[]) => {
const tags: DocumentTag[] = []
TAG_SLOTS.forEach((slot) => {
const value = docData[slot] as string | null | undefined
const definition = definitions.find((def) => def.tagSlot === slot)
if (value?.trim() && definition) {
tags.push({
slot,
displayName: definition.displayName,
fieldType: definition.fieldType,
value: value.trim(),
})
}
})
return tags
}, [])
const handleTagsChange = useCallback((newTags: DocumentTag[]) => {
setDocumentTags(newTags)
}, [])
const handleSaveDocumentTags = useCallback(
async (tagsToSave: DocumentTag[]) => {
if (!documentData) return
try {
const tagData: Record<string, string> = {}
TAG_SLOTS.forEach((slot) => {
tagData[slot] = ''
})
tagsToSave.forEach((tag) => {
if (tag.value.trim()) {
tagData[tag.slot] = tag.value.trim()
}
})
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/documents/${documentId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tagData),
})
if (!response.ok) {
throw new Error('Failed to update document tags')
}
updateDocumentInStore(knowledgeBaseId, documentId, tagData)
onDocumentUpdate?.(tagData)
await fetchTagDefinitions()
} catch (error) {
logger.error('Error updating document tags:', error)
throw error
}
},
[
documentData,
knowledgeBaseId,
documentId,
updateDocumentInStore,
fetchTagDefinitions,
onDocumentUpdate,
]
)
const handleRemoveTag = async (index: number) => {
const updatedTags = documentTags.filter((_, i) => i !== index)
handleTagsChange(updatedTags)
try {
await handleSaveDocumentTags(updatedTags)
} catch (error) {
logger.error('Error removing tag:', error)
}
}
const startEditingTag = (index: number) => {
const tag = documentTags[index]
setEditingTagIndex(index)
setEditTagForm({
displayName: tag.displayName,
fieldType: tag.fieldType,
value: tag.value,
})
setIsCreatingTag(false)
}
const openTagCreator = () => {
setEditingTagIndex(null)
setEditTagForm({
displayName: '',
fieldType: 'text',
value: '',
})
setIsCreatingTag(true)
}
const cancelEditingTag = () => {
setEditTagForm({
displayName: '',
fieldType: 'text',
value: '',
})
setEditingTagIndex(null)
setIsCreatingTag(false)
}
const hasTagNameConflict = (name: string) => {
if (!name.trim()) return false
return documentTags.some((tag, index) => {
if (editingTagIndex !== null && index === editingTagIndex) {
return false
}
return tag.displayName.toLowerCase() === name.trim().toLowerCase()
})
}
const availableDefinitions = kbTagDefinitions.filter((def) => {
return !documentTags.some(
(tag) => tag.displayName.toLowerCase() === def.displayName.toLowerCase()
)
})
const tagNameOptions = availableDefinitions.map((def) => ({
label: def.displayName,
value: def.displayName,
}))
const saveDocumentTag = async () => {
if (!editTagForm.displayName.trim() || !editTagForm.value.trim()) return
const formData = { ...editTagForm }
const currentEditingIndex = editingTagIndex
const originalTag = currentEditingIndex !== null ? documentTags[currentEditingIndex] : null
setEditingTagIndex(null)
setIsCreatingTag(false)
setIsSavingTag(true)
try {
let targetSlot: string
if (currentEditingIndex !== null && originalTag) {
targetSlot = originalTag.slot
} else {
const existingDefinition = kbTagDefinitions.find(
(def) => def.displayName.toLowerCase() === formData.displayName.toLowerCase()
)
if (existingDefinition) {
targetSlot = existingDefinition.tagSlot
} else {
const serverSlot = await getServerNextSlot(formData.fieldType)
if (!serverSlot) {
throw new Error(`No available slots for new tag of type '${formData.fieldType}'`)
}
targetSlot = serverSlot
}
}
let updatedTags: DocumentTag[]
if (currentEditingIndex !== null) {
updatedTags = [...documentTags]
updatedTags[currentEditingIndex] = {
...updatedTags[currentEditingIndex],
displayName: formData.displayName,
fieldType: formData.fieldType,
value: formData.value,
}
} else {
const newTag: DocumentTag = {
slot: targetSlot,
displayName: formData.displayName,
fieldType: formData.fieldType,
value: formData.value,
}
updatedTags = [...documentTags, newTag]
}
handleTagsChange(updatedTags)
if (currentEditingIndex !== null && originalTag) {
const currentDefinition = kbTagDefinitions.find(
(def) => def.displayName.toLowerCase() === originalTag.displayName.toLowerCase()
)
if (currentDefinition) {
const updatedDefinition: TagDefinitionInput = {
displayName: formData.displayName,
fieldType: currentDefinition.fieldType,
tagSlot: currentDefinition.tagSlot,
_originalDisplayName: originalTag.displayName,
}
if (saveTagDefinitions) {
await saveTagDefinitions([updatedDefinition])
}
await refreshTagDefinitions()
}
} else {
const existingDefinition = kbTagDefinitions.find(
(def) => def.displayName.toLowerCase() === formData.displayName.toLowerCase()
)
if (!existingDefinition) {
const newDefinition: TagDefinitionInput = {
displayName: formData.displayName,
fieldType: formData.fieldType,
tagSlot: targetSlot as TagSlot,
}
if (saveTagDefinitions) {
await saveTagDefinitions([newDefinition])
}
await refreshTagDefinitions()
}
}
await handleSaveDocumentTags(updatedTags)
setEditTagForm({
displayName: '',
fieldType: 'text',
value: '',
})
} catch (error) {
logger.error('Error saving tag:', error)
} finally {
setIsSavingTag(false)
}
}
const isTagEditing = editingTagIndex !== null || isCreatingTag
const tagNameConflict = hasTagNameConflict(editTagForm.displayName)
const hasTagChanges = () => {
if (editingTagIndex === null) return true
const originalTag = documentTags[editingTagIndex]
if (!originalTag) return true
return (
originalTag.displayName !== editTagForm.displayName ||
originalTag.value !== editTagForm.value ||
originalTag.fieldType !== editTagForm.fieldType
)
}
const canSaveTag =
editTagForm.displayName.trim() &&
editTagForm.value.trim() &&
!tagNameConflict &&
hasTagChanges()
const canAddNewTag = kbTagDefinitions.length < MAX_TAG_SLOTS || availableDefinitions.length > 0
useEffect(() => {
if (documentData && tagDefinitions && !isSavingTag) {
const rebuiltTags = buildDocumentTags(documentData, tagDefinitions)
setDocumentTags(rebuiltTags)
}
}, [documentData, tagDefinitions, buildDocumentTags, isSavingTag])
const handleClose = (openState: boolean) => {
if (!openState) {
setIsCreatingTag(false)
setEditingTagIndex(null)
setEditTagForm({
displayName: '',
fieldType: 'text',
value: '',
})
}
onOpenChange(openState)
}
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalHeader>
<div className='flex items-center justify-between'>
<span>Document Tags</span>
</div>
</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[8px]'>
<Label>
Tags{' '}
<span className='pl-[6px] text-[var(--text-tertiary)]'>
{documentTags.length}/{MAX_TAG_SLOTS} slots used
</span>
</Label>
{documentTags.length === 0 && !isCreatingTag && (
<div className='rounded-[6px] border p-[16px] text-center'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
No tags added yet. Add tags to help organize this document.
</p>
</div>
)}
{documentTags.map((tag, index) => (
<div key={index} className='space-y-[8px]'>
<div
className='flex cursor-pointer items-center gap-2 rounded-[4px] border p-[8px] hover:bg-[var(--surface-2)]'
onClick={() => startEditingTag(index)}
>
<span className='min-w-0 truncate text-[12px] text-[var(--text-primary)]'>
{tag.displayName}
</span>
<div className='mb-[-1.5px] h-[14px] w-[1.25px] flex-shrink-0 rounded-full bg-[#3A3A3A]' />
<span className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-muted)]'>
{tag.value}
</span>
<div className='flex flex-shrink-0 items-center gap-1'>
<Button
variant='ghost'
onClick={(e) => {
e.stopPropagation()
handleRemoveTag(index)
}}
className='h-4 w-4 p-0 text-[var(--text-muted)] hover:text-[var(--text-error)]'
>
<Trash className='h-3 w-3' />
</Button>
</div>
</div>
{editingTagIndex === index && (
<div className='space-y-[8px] rounded-[6px] border p-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor={`tagName-${index}`}>Tag Name</Label>
{availableDefinitions.length > 0 ? (
<Combobox
id={`tagName-${index}`}
options={tagNameOptions}
value={editTagForm.displayName}
selectedValue={editTagForm.displayName}
onChange={(value) => {
const def = kbTagDefinitions.find(
(d) => d.displayName.toLowerCase() === value.toLowerCase()
)
setEditTagForm({
...editTagForm,
displayName: value,
fieldType: def?.fieldType || 'text',
})
}}
placeholder='Enter or select tag name'
editable={true}
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
/>
) : (
<Input
id={`tagName-${index}`}
value={editTagForm.displayName}
onChange={(e) =>
setEditTagForm({ ...editTagForm, displayName: e.target.value })
}
placeholder='Enter tag name'
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
onKeyDown={(e) => {
if (e.key === 'Enter' && canSaveTag) {
e.preventDefault()
saveDocumentTag()
}
if (e.key === 'Escape') {
e.preventDefault()
cancelEditingTag()
}
}}
/>
)}
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}
</div>
{/* Type selector commented out - only "text" type is currently supported
<div className='flex flex-col gap-[8px]'>
<Label htmlFor={`tagType-${index}`}>Type</Label>
<Input id={`tagType-${index}`} value='Text' disabled className='capitalize' />
</div>
*/}
<div className='flex flex-col gap-[8px]'>
<Label htmlFor={`tagValue-${index}`}>Value</Label>
<Input
id={`tagValue-${index}`}
value={editTagForm.value}
onChange={(e) =>
setEditTagForm({ ...editTagForm, value: e.target.value })
}
placeholder='Enter tag value'
onKeyDown={(e) => {
if (e.key === 'Enter' && canSaveTag) {
e.preventDefault()
saveDocumentTag()
}
if (e.key === 'Escape') {
e.preventDefault()
cancelEditingTag()
}
}}
/>
</div>
<div className='flex gap-[8px]'>
<Button variant='default' onClick={cancelEditingTag} className='flex-1'>
Cancel
</Button>
<Button
variant='primary'
onClick={saveDocumentTag}
className='flex-1'
disabled={!canSaveTag}
>
Save Changes
</Button>
</div>
</div>
)}
</div>
))}
{!isTagEditing && (
<Button
variant='default'
onClick={openTagCreator}
disabled={!canAddNewTag}
className='w-full'
>
Add Tag
</Button>
)}
{isCreatingTag && (
<div className='space-y-[8px] rounded-[6px] border p-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='newTagName'>Tag Name</Label>
{tagNameOptions.length > 0 ? (
<Combobox
id='newTagName'
options={tagNameOptions}
value={editTagForm.displayName}
selectedValue={editTagForm.displayName}
onChange={(value) => {
const def = kbTagDefinitions.find(
(d) => d.displayName.toLowerCase() === value.toLowerCase()
)
setEditTagForm({
...editTagForm,
displayName: value,
fieldType: def?.fieldType || 'text',
})
}}
placeholder='Enter or select tag name'
editable={true}
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
/>
) : (
<Input
id='newTagName'
value={editTagForm.displayName}
onChange={(e) =>
setEditTagForm({ ...editTagForm, displayName: e.target.value })
}
placeholder='Enter tag name'
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
onKeyDown={(e) => {
if (e.key === 'Enter' && canSaveTag) {
e.preventDefault()
saveDocumentTag()
}
if (e.key === 'Escape') {
e.preventDefault()
cancelEditingTag()
}
}}
/>
)}
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}
</div>
{/* Type selector commented out - only "text" type is currently supported
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='newTagType'>Type</Label>
<Input id='newTagType' value='Text' disabled className='capitalize' />
</div>
*/}
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='newTagValue'>Value</Label>
<Input
id='newTagValue'
value={editTagForm.value}
onChange={(e) => setEditTagForm({ ...editTagForm, value: e.target.value })}
placeholder='Enter tag value'
onKeyDown={(e) => {
if (e.key === 'Enter' && canSaveTag) {
e.preventDefault()
saveDocumentTag()
}
if (e.key === 'Escape') {
e.preventDefault()
cancelEditingTag()
}
}}
/>
</div>
{kbTagDefinitions.length >= MAX_TAG_SLOTS &&
!kbTagDefinitions.find(
(def) =>
def.displayName.toLowerCase() === editTagForm.displayName.toLowerCase()
) && (
<div className='rounded-[4px] border border-amber-500/50 bg-amber-500/10 p-[8px]'>
<p className='text-[11px] text-amber-600 dark:text-amber-400'>
Maximum tag definitions reached. You can still use existing tag
definitions, but cannot create new ones.
</p>
</div>
)}
<div className='flex gap-[8px]'>
<Button variant='default' onClick={cancelEditingTag} className='flex-1'>
Cancel
</Button>
<Button
variant='primary'
onClick={saveDocumentTag}
className='flex-1'
disabled={
!canSaveTag ||
isSavingTag ||
(kbTagDefinitions.length >= MAX_TAG_SLOTS &&
!kbTagDefinitions.find(
(def) =>
def.displayName.toLowerCase() ===
editTagForm.displayName.toLowerCase()
))
}
>
{isSavingTag ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
</Button>
</div>
</div>
)}
</div>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => handleClose(false)}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { AlertCircle, ChevronDown, ChevronUp, Loader2, X } from 'lucide-react'
import {
Button,
@@ -171,13 +172,15 @@ export function EditChunkModal({
return (
<>
<Modal open={isOpen} onOpenChange={handleCloseAttempt}>
<ModalContent className='h-[74vh] sm:max-w-[600px]'>
<ModalContent size='lg'>
<div className='flex items-center justify-between px-[16px] py-[10px]'>
<div className='flex items-center gap-3'>
<span className='font-medium text-[16px] text-[var(--text-primary)]'>Edit Chunk</span>
<DialogPrimitive.Title className='font-medium text-[16px] text-[var(--text-primary)]'>
Edit Chunk #{chunk.chunkIndex}
</DialogPrimitive.Title>
<div className='flex flex-shrink-0 items-center gap-[8px]'>
{/* Navigation Controls */}
<div className='flex items-center gap-1'>
<div className='flex items-center gap-[6px]'>
<Tooltip.Root>
<Tooltip.Trigger
asChild
@@ -188,9 +191,9 @@ export function EditChunkModal({
variant='ghost'
onClick={() => handleNavigate('prev')}
disabled={!canNavigatePrev || isNavigating || isSaving}
className='h-8 w-8 p-0'
className='h-[16px] w-[16px] p-0'
>
<ChevronUp className='h-4 w-4' />
<ChevronUp className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
@@ -209,9 +212,9 @@ export function EditChunkModal({
variant='ghost'
onClick={() => handleNavigate('next')}
disabled={!canNavigateNext || isNavigating || isSaving}
className='h-8 w-8 p-0'
className='h-[16px] w-[16px] p-0'
>
<ChevronDown className='h-4 w-4' />
<ChevronDown className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
@@ -222,29 +225,21 @@ export function EditChunkModal({
</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
<Button variant='ghost' className='h-[16px] w-[16px] p-0' onClick={handleCloseAttempt}>
<X className='h-[16px] w-[16px]' />
<span className='sr-only'>Close</span>
</Button>
<Button
variant='ghost'
className='h-[16px] w-[16px] p-0'
onClick={handleCloseAttempt}
>
<X className='h-[16px] w-[16px]' />
<span className='sr-only'>Close</span>
</Button>
</div>
</div>
<form className='flex min-h-0 flex-1 flex-col'>
<ModalBody>
<div className='space-y-[12px]'>
{/* Document Info Section */}
<div className='flex items-center gap-3 rounded-lg border p-4'>
<div className='min-w-0 flex-1'>
<p className='font-medium text-[var(--text-primary)] text-sm'>
{document?.filename || 'Unknown Document'}
</p>
<p className='text-[var(--text-tertiary)] text-xs'>
Editing chunk #{chunk.chunkIndex} Page {currentPage} of {totalPages}
</p>
</div>
</div>
<form>
<ModalBody className='!pb-[16px]'>
<div className='flex flex-col gap-[8px]'>
{/* Error Display */}
{error && (
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
@@ -254,25 +249,18 @@ export function EditChunkModal({
)}
{/* Content Input Section */}
<div className='space-y-[8px]'>
<Label
htmlFor='content'
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
>
Chunk Content
</Label>
<Textarea
id='content'
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
placeholder={
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
}
rows={10}
disabled={isSaving || isNavigating || !userPermissions.canEdit}
readOnly={!userPermissions.canEdit}
/>
</div>
<Label htmlFor='content'>Chunk</Label>
<Textarea
id='content'
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
placeholder={
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
}
rows={20}
disabled={isSaving || isNavigating || !userPermissions.canEdit}
readOnly={!userPermissions.canEdit}
/>
</div>
</ModalBody>
@@ -298,7 +286,7 @@ export function EditChunkModal({
Saving...
</>
) : (
'Save Changes'
'Save'
)}
</Button>
)}
@@ -309,7 +297,7 @@ export function EditChunkModal({
{/* Unsaved Changes Alert */}
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
<ModalContent className='w-[400px]'>
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>

View File

@@ -1,4 +1,4 @@
export { CreateChunkModal } from './create-chunk-modal/create-chunk-modal'
export { DeleteChunkModal } from './delete-chunk-modal/delete-chunk-modal'
export { DocumentLoading } from './document-loading'
export { DocumentTagsModal } from './document-tags-modal/document-tags-modal'
export { EditChunkModal } from './edit-chunk-modal/edit-chunk-modal'

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import { motion } from 'framer-motion'
import { Circle, CircleOff, Trash2 } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { Circle, CircleOff } from 'lucide-react'
import { Button, Tooltip, Trash2 } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -42,23 +41,22 @@ export function ActionBar({
transition={{ duration: 0.2 }}
className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)}
>
<div className='flex items-center gap-3 rounded-lg border border-gray-200 bg-background px-4 py-2 shadow-sm dark:border-gray-800'>
<span className='text-gray-500 text-sm'>{selectedCount} selected</span>
<div className='flex items-center gap-[8px] rounded-[10px] border border-[var(--border-strong)] bg-[var(--surface-1)] p-[8px]'>
<span className='px-[4px] text-[13px] text-[var(--text-muted)]'>
{selectedCount} selected
</span>
<div className='h-4 w-px bg-gray-200 dark:bg-gray-800' />
<div className='flex items-center gap-1'>
<div className='flex items-center gap-[5px]'>
{showEnableButton && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
size='sm'
onClick={onEnable}
disabled={isLoading}
className='text-gray-500 hover:text-gray-700'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
>
<Circle className='h-4 w-4' />
<Circle className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
@@ -72,12 +70,11 @@ export function ActionBar({
<Tooltip.Trigger asChild>
<Button
variant='ghost'
size='sm'
onClick={onDisable}
disabled={isLoading}
className='text-gray-500 hover:text-gray-700'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
>
<CircleOff className='h-4 w-4' />
<CircleOff className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
@@ -91,12 +88,11 @@ export function ActionBar({
<Tooltip.Trigger asChild>
<Button
variant='ghost'
size='sm'
onClick={onDelete}
disabled={isLoading}
className='text-gray-500 hover:text-red-600'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
>
<Trash2 className='h-4 w-4' />
<Trash2 className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Delete items</Tooltip.Content>

View File

@@ -0,0 +1,366 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { AlertCircle, Loader2, RotateCcw, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Button,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/emcn'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
const logger = createLogger('AddDocumentsModal')
interface FileWithPreview extends File {
preview: string
}
interface AddDocumentsModalProps {
open: boolean
onOpenChange: (open: boolean) => void
knowledgeBaseId: string
chunkingConfig?: {
maxSize: number
minSize: number
overlap: number
}
onUploadComplete?: () => void
}
export function AddDocumentsModal({
open,
onOpenChange,
knowledgeBaseId,
chunkingConfig,
onUploadComplete,
}: AddDocumentsModalProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const fileInputRef = useRef<HTMLInputElement>(null)
const [files, setFiles] = useState<FileWithPreview[]>([])
const [fileError, setFileError] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const [dragCounter, setDragCounter] = useState(0)
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
const { isUploading, uploadProgress, uploadFiles, clearError } = useKnowledgeUpload({
workspaceId,
onUploadComplete: () => {
logger.info(`Successfully uploaded ${files.length} files`)
onUploadComplete?.()
handleClose()
},
})
useEffect(() => {
return () => {
files.forEach((file) => {
if (file.preview) {
URL.revokeObjectURL(file.preview)
}
})
}
}, [files])
useEffect(() => {
if (open) {
setFiles([])
setFileError(null)
setIsDragging(false)
setDragCounter(0)
setRetryingIndexes(new Set())
clearError()
}
}, [open, clearError])
const handleClose = () => {
if (isUploading) return
setFiles([])
setFileError(null)
clearError()
setIsDragging(false)
setDragCounter(0)
setRetryingIndexes(new Set())
onOpenChange(false)
}
const processFiles = async (fileList: FileList | File[]) => {
setFileError(null)
if (!fileList || fileList.length === 0) return
try {
const newFiles: FileWithPreview[] = []
let hasError = false
for (const file of Array.from(fileList)) {
const validationError = validateKnowledgeBaseFile(file)
if (validationError) {
setFileError(validationError)
hasError = true
continue
}
const fileWithPreview = Object.assign(file, {
preview: URL.createObjectURL(file),
}) as FileWithPreview
newFiles.push(fileWithPreview)
}
if (!hasError && newFiles.length > 0) {
setFiles((prev) => [...prev, ...newFiles])
}
} catch (error) {
logger.error('Error processing files:', error)
setFileError('An error occurred while processing files. Please try again.')
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
}
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
await processFiles(e.target.files)
}
}
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragCounter((prev) => {
const newCount = prev + 1
if (newCount === 1) {
setIsDragging(true)
}
return newCount
})
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragCounter((prev) => {
const newCount = prev - 1
if (newCount === 0) {
setIsDragging(false)
}
return newCount
})
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'copy'
}
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
setDragCounter(0)
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
await processFiles(e.dataTransfer.files)
}
}
const removeFile = (index: number) => {
setFiles((prev) => {
URL.revokeObjectURL(prev[index].preview)
return prev.filter((_, i) => i !== index)
})
}
const handleRetryFile = async (index: number) => {
const fileToRetry = files[index]
if (!fileToRetry) return
setRetryingIndexes((prev) => new Set(prev).add(index))
try {
await uploadFiles([fileToRetry], knowledgeBaseId, {
chunkSize: chunkingConfig?.maxSize || 1024,
minCharactersPerChunk: chunkingConfig?.minSize || 1,
chunkOverlap: chunkingConfig?.overlap || 200,
recipe: 'default',
})
removeFile(index)
} catch (error) {
logger.error('Error retrying file upload:', error)
} finally {
setRetryingIndexes((prev) => {
const newSet = new Set(prev)
newSet.delete(index)
return newSet
})
}
}
const handleUpload = async () => {
if (files.length === 0) return
try {
await uploadFiles(files, knowledgeBaseId, {
chunkSize: chunkingConfig?.maxSize || 1024,
minCharactersPerChunk: chunkingConfig?.minSize || 1,
chunkOverlap: chunkingConfig?.overlap || 200,
recipe: 'default',
})
} catch (error) {
logger.error('Error uploading files:', error)
}
}
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalHeader>Add Documents</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[12px]'>
{fileError && (
<Alert variant='destructive'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{fileError}</AlertDescription>
</Alert>
)}
<div className='flex flex-col gap-[8px]'>
<Label>Upload Documents</Label>
<Button
type='button'
variant='default'
onClick={() => fileInputRef.current?.click()}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
'!bg-[var(--surface-1)] hover:!bg-[var(--surface-4)] w-full justify-center border border-[var(--c-575757)] border-dashed py-[10px]',
isDragging && 'border-[var(--brand-primary-hex)]'
)}
>
<input
ref={fileInputRef}
type='file'
accept={ACCEPT_ATTRIBUTE}
onChange={handleFileChange}
className='hidden'
multiple
/>
<div className='flex flex-col gap-[2px] text-center'>
<span className='text-[var(--text-primary)]'>
{isDragging ? 'Drop files here' : 'Drop files here or click to browse'}
</span>
<span className='text-[11px] text-[var(--text-tertiary)]'>
PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML (max 100MB each)
</span>
</div>
</Button>
</div>
{files.length > 0 && (
<div className='space-y-2'>
<Label>Selected Files</Label>
<div className='space-y-2'>
{files.map((file, index) => {
const fileStatus = uploadProgress.fileStatuses?.[index]
const isFailed = fileStatus?.status === 'failed'
const isRetrying = retryingIndexes.has(index)
const isProcessing = fileStatus?.status === 'uploading' || isRetrying
return (
<div
key={index}
className='flex items-center gap-2 rounded-[4px] border p-[8px]'
>
{isFailed && !isRetrying && (
<AlertCircle className='h-4 w-4 flex-shrink-0 text-[var(--text-error)]' />
)}
<span
className={cn(
'min-w-0 flex-1 truncate text-[12px]',
isFailed && !isRetrying && 'text-[var(--text-error)]'
)}
title={file.name}
>
{file.name}
</span>
<span className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
{formatFileSize(file.size)}
</span>
<div className='flex flex-shrink-0 items-center gap-1'>
{isFailed && !isRetrying && (
<Button
type='button'
variant='ghost'
className='h-4 w-4 p-0 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
onClick={() => handleRetryFile(index)}
disabled={isUploading}
>
<RotateCcw className='h-3.5 w-3.5' />
</Button>
)}
{isProcessing ? (
<Loader2 className='h-4 w-4 animate-spin text-[var(--text-muted)]' />
) : (
<Button
type='button'
variant='ghost'
className='h-4 w-4 p-0'
onClick={() => removeFile(index)}
disabled={isUploading}
>
<X className='h-3.5 w-3.5' />
</Button>
)}
</div>
</div>
)
})}
</div>
</div>
)}
</div>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={handleClose} type='button' disabled={isUploading}>
Cancel
</Button>
<Button
variant='primary'
type='button'
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
>
{isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Uploading...'
: 'Upload'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}

View File

@@ -0,0 +1,485 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Trash,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { MAX_TAG_SLOTS } from '@/lib/knowledge/constants'
import { createLogger } from '@/lib/logs/console/logger'
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
import {
type TagDefinition,
useKnowledgeBaseTagDefinitions,
} from '@/hooks/use-knowledge-base-tag-definitions'
const logger = createLogger('BaseTagsModal')
interface TagUsageData {
tagName: string
tagSlot: string
documentCount: number
documents: Array<{ id: string; name: string; tagValue: string }>
}
interface DocumentListProps {
documents: Array<{ id: string; name: string; tagValue: string }>
totalCount: number
}
/** Displays a list of documents affected by tag operations */
function DocumentList({ documents, totalCount }: DocumentListProps) {
const displayLimit = 5
const hasMore = totalCount > displayLimit
return (
<div className='rounded-[4px] border'>
<div className='max-h-[160px] overflow-y-auto'>
{documents.slice(0, displayLimit).map((doc) => {
const DocumentIcon = getDocumentIcon('', doc.name)
return (
<div
key={doc.id}
className='flex items-center gap-[8px] border-b p-[8px] last:border-b-0'
>
<DocumentIcon className='h-4 w-4 flex-shrink-0 text-[var(--text-muted)]' />
<span className='min-w-0 max-w-[120px] truncate text-[12px] text-[var(--text-primary)]'>
{doc.name}
</span>
{doc.tagValue && (
<>
<div className='mb-[-1.5px] h-[14px] w-[1.25px] flex-shrink-0 rounded-full bg-[#3A3A3A]' />
<span className='min-w-0 flex-1 truncate text-[11px] text-[var(--text-muted)]'>
{doc.tagValue}
</span>
</>
)}
</div>
)
})}
{hasMore && (
<div className='p-[8px] text-[11px] text-[var(--text-muted)]'>
and {totalCount - displayLimit} more documents
</div>
)}
</div>
</div>
)
}
interface BaseTagsModalProps {
open: boolean
onOpenChange: (open: boolean) => void
knowledgeBaseId: string
}
export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsModalProps) {
const { tagDefinitions: kbTagDefinitions, fetchTagDefinitions: refreshTagDefinitions } =
useKnowledgeBaseTagDefinitions(knowledgeBaseId)
const [deleteTagDialogOpen, setDeleteTagDialogOpen] = useState(false)
const [selectedTag, setSelectedTag] = useState<TagDefinition | null>(null)
const [viewDocumentsDialogOpen, setViewDocumentsDialogOpen] = useState(false)
const [isDeletingTag, setIsDeletingTag] = useState(false)
const [tagUsageData, setTagUsageData] = useState<TagUsageData[]>([])
const [isCreatingTag, setIsCreatingTag] = useState(false)
const [isSavingTag, setIsSavingTag] = useState(false)
const [createTagForm, setCreateTagForm] = useState({
displayName: '',
fieldType: 'text',
})
const fetchTagUsage = useCallback(async () => {
if (!knowledgeBaseId) return
try {
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/tag-usage`)
if (!response.ok) {
throw new Error('Failed to fetch tag usage')
}
const result = await response.json()
if (result.success) {
setTagUsageData(result.data)
}
} catch (error) {
logger.error('Error fetching tag usage:', error)
}
}, [knowledgeBaseId])
useEffect(() => {
if (open) {
fetchTagUsage()
}
}, [open, fetchTagUsage])
const getTagUsage = (tagSlot: string): TagUsageData => {
return (
tagUsageData.find((usage) => usage.tagSlot === tagSlot) || {
tagName: '',
tagSlot,
documentCount: 0,
documents: [],
}
)
}
const handleDeleteTagClick = async (tag: TagDefinition) => {
setSelectedTag(tag)
await fetchTagUsage()
setDeleteTagDialogOpen(true)
}
const handleViewDocuments = async (tag: TagDefinition) => {
setSelectedTag(tag)
await fetchTagUsage()
setViewDocumentsDialogOpen(true)
}
const openTagCreator = () => {
setCreateTagForm({
displayName: '',
fieldType: 'text',
})
setIsCreatingTag(true)
}
const cancelCreatingTag = () => {
setCreateTagForm({
displayName: '',
fieldType: 'text',
})
setIsCreatingTag(false)
}
const hasTagNameConflict = (name: string) => {
if (!name.trim()) return false
return kbTagDefinitions.some(
(tag) => tag.displayName.toLowerCase() === name.trim().toLowerCase()
)
}
const tagNameConflict =
isCreatingTag && !isSavingTag && hasTagNameConflict(createTagForm.displayName)
const canSaveTag = () => {
return createTagForm.displayName.trim() && !hasTagNameConflict(createTagForm.displayName)
}
const saveTagDefinition = async () => {
if (!canSaveTag()) return
setIsSavingTag(true)
try {
const usedSlots = new Set(kbTagDefinitions.map((def) => def.tagSlot))
const availableSlot = (
['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'] as const
).find((slot) => !usedSlots.has(slot))
if (!availableSlot) {
throw new Error('No available tag slots')
}
const newTagDefinition = {
tagSlot: availableSlot,
displayName: createTagForm.displayName.trim(),
fieldType: createTagForm.fieldType,
}
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/tag-definitions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTagDefinition),
})
if (!response.ok) {
throw new Error('Failed to create tag definition')
}
await Promise.all([refreshTagDefinitions(), fetchTagUsage()])
setCreateTagForm({
displayName: '',
fieldType: 'text',
})
setIsCreatingTag(false)
} catch (error) {
logger.error('Error creating tag definition:', error)
} finally {
setIsSavingTag(false)
}
}
const confirmDeleteTag = async () => {
if (!selectedTag) return
setIsDeletingTag(true)
try {
const response = await fetch(
`/api/knowledge/${knowledgeBaseId}/tag-definitions/${selectedTag.id}`,
{
method: 'DELETE',
}
)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to delete tag definition: ${response.status} ${errorText}`)
}
await Promise.all([refreshTagDefinitions(), fetchTagUsage()])
setDeleteTagDialogOpen(false)
setSelectedTag(null)
} catch (error) {
logger.error('Error deleting tag definition:', error)
} finally {
setIsDeletingTag(false)
}
}
const selectedTagUsage = selectedTag ? getTagUsage(selectedTag.tagSlot) : null
const handleClose = (openState: boolean) => {
if (!openState) {
setIsCreatingTag(false)
setCreateTagForm({
displayName: '',
fieldType: 'text',
})
}
onOpenChange(openState)
}
return (
<>
<Modal open={open} onOpenChange={handleClose}>
<ModalContent>
<ModalHeader>
<div className='flex items-center justify-between'>
<span>Tags</span>
</div>
</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[8px]'>
<Label>
Tags:{' '}
<span className='pl-[6px] text-[var(--text-tertiary)]'>
{kbTagDefinitions.length}/{MAX_TAG_SLOTS} slots used
</span>
</Label>
{kbTagDefinitions.length === 0 && !isCreatingTag && (
<div className='rounded-[6px] border p-[16px] text-center'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
No tag definitions yet. Create your first tag to organize documents.
</p>
</div>
)}
{kbTagDefinitions.map((tag) => {
const usage = getTagUsage(tag.tagSlot)
return (
<div
key={tag.id}
className='flex cursor-pointer items-center gap-2 rounded-[4px] border p-[8px] hover:bg-[var(--surface-2)]'
onClick={() => handleViewDocuments(tag)}
>
<span className='min-w-0 truncate text-[12px] text-[var(--text-primary)]'>
{tag.displayName}
</span>
<div className='mb-[-1.5px] h-[14px] w-[1.25px] flex-shrink-0 rounded-full bg-[#3A3A3A]' />
<span className='min-w-0 flex-1 text-[11px] text-[var(--text-muted)]'>
{usage.documentCount} document{usage.documentCount !== 1 ? 's' : ''}
</span>
<div className='flex flex-shrink-0 items-center gap-1'>
<Button
variant='ghost'
onClick={(e) => {
e.stopPropagation()
handleDeleteTagClick(tag)
}}
className='h-4 w-4 p-0 text-[var(--text-muted)] hover:text-[var(--text-error)]'
>
<Trash className='h-3 w-3' />
</Button>
</div>
</div>
)
})}
{!isCreatingTag && (
<Button
variant='default'
onClick={openTagCreator}
disabled={kbTagDefinitions.length >= MAX_TAG_SLOTS}
className='w-full'
>
Add Tag
</Button>
)}
{isCreatingTag && (
<div className='space-y-[8px] rounded-[6px] border p-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='tagName'>Tag Name</Label>
<Input
id='tagName'
value={createTagForm.displayName}
onChange={(e) =>
setCreateTagForm({ ...createTagForm, displayName: e.target.value })
}
placeholder='Enter tag name'
className={cn(tagNameConflict && 'border-[var(--text-error)]')}
onKeyDown={(e) => {
if (e.key === 'Enter' && canSaveTag()) {
e.preventDefault()
saveTagDefinition()
}
if (e.key === 'Escape') {
e.preventDefault()
cancelCreatingTag()
}
}}
/>
{tagNameConflict && (
<span className='text-[11px] text-[var(--text-error)]'>
A tag with this name already exists
</span>
)}
</div>
{/* Type selector commented out - only "text" type is currently supported
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='tagType'>Type</Label>
<Input id='tagType' value='Text' disabled className='capitalize' />
</div>
*/}
<div className='flex gap-[8px]'>
<Button variant='default' onClick={cancelCreatingTag} className='flex-1'>
Cancel
</Button>
<Button
variant='primary'
onClick={saveTagDefinition}
className='flex-1'
disabled={!canSaveTag() || isSavingTag}
>
{isSavingTag ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
</Button>
</div>
</div>
)}
</div>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => handleClose(false)}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Delete Tag Confirmation Dialog */}
<Modal open={deleteTagDialogOpen} onOpenChange={setDeleteTagDialogOpen}>
<ModalContent size='sm'>
<ModalHeader>Delete Tag</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
remove this tag from {selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
{selectedTagUsage && selectedTagUsage.documentCount > 0 && (
<div className='flex flex-col gap-[8px]'>
<Label>Affected documents:</Label>
<DocumentList
documents={selectedTagUsage.documents}
totalCount={selectedTagUsage.documentCount}
/>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button
variant='default'
disabled={isDeletingTag}
onClick={() => setDeleteTagDialogOpen(false)}
>
Cancel
</Button>
<Button
variant='primary'
onClick={confirmDeleteTag}
disabled={isDeletingTag}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeletingTag ? <>Deleting...</> : 'Delete Tag'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* View Documents Dialog */}
<Modal open={viewDocumentsDialogOpen} onOpenChange={setViewDocumentsDialogOpen}>
<ModalContent size='sm'>
<ModalHeader>Documents using "{selectedTag?.displayName}"</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
{selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's are' : ' is'} currently using this tag
definition.
</p>
{selectedTagUsage?.documentCount === 0 ? (
<div className='rounded-[6px] border p-[16px] text-center'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
This tag definition is not being used by any documents. You can safely delete it
to free up the tag slot.
</p>
</div>
) : (
<DocumentList
documents={selectedTagUsage?.documents || []}
totalCount={selectedTagUsage?.documentCount || 0}
/>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => setViewDocumentsDialogOpen(false)}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}

View File

@@ -1,3 +1,3 @@
export { ActionBar } from './action-bar/action-bar'
export { KnowledgeBaseLoading } from './knowledge-base-loading/knowledge-base-loading'
export { UploadModal } from './upload-modal/upload-modal'
export { AddDocumentsModal } from './add-documents-modal/add-documents-modal'
export { BaseTagsModal } from './base-tags-modal/base-tags-modal'

View File

@@ -1,72 +0,0 @@
'use client'
import { Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import {
DocumentTableSkeleton,
KnowledgeHeader,
} from '@/app/workspace/[workspaceId]/knowledge/components'
interface KnowledgeBaseLoadingProps {
knowledgeBaseName: string
}
export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps) {
const params = useParams()
const workspaceId = params?.workspaceId as string
const breadcrumbs = [
{
id: 'knowledge-root',
label: 'Knowledge',
href: `/workspace/${workspaceId}/knowledge`,
},
{
id: 'knowledge-base-loading',
label: knowledgeBaseName,
},
]
return (
<div className='flex h-[100vh] flex-col pl-64'>
{/* Fixed Header with Breadcrumbs */}
<KnowledgeHeader breadcrumbs={breadcrumbs} />
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-hidden'>
{/* Main Content */}
<div className='flex-1 overflow-auto'>
<div className='px-6 pb-6'>
{/* Search and Create Section */}
<div className='mb-4 flex items-center justify-between pt-1'>
<div className='relative max-w-md flex-1'>
<div className='relative flex items-center'>
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-[18px] w-[18px] transform text-muted-foreground' />
<input
type='text'
placeholder='Search documents...'
disabled
className='h-10 w-full rounded-md border bg-background px-9 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'
/>
</div>
</div>
<div className='flex items-center gap-2'>
{/* Add Documents Button - disabled state */}
<Button disabled variant='primary' className='flex items-center gap-1'>
<div className='h-3.5 w-3.5 animate-pulse rounded bg-primary-foreground/30' />
<span>Add Documents</span>
</Button>
</div>
</div>
{/* Table container */}
<DocumentTableSkeleton isSidebarCollapsed={false} rowCount={8} />
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,315 +0,0 @@
'use client'
import { useRef, useState } from 'react'
import { AlertCircle, Check, Loader2, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import { createLogger } from '@/lib/logs/console/logger'
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
const logger = createLogger('UploadModal')
interface FileWithPreview extends File {
preview: string
}
interface UploadModalProps {
open: boolean
onOpenChange: (open: boolean) => void
knowledgeBaseId: string
chunkingConfig?: {
maxSize: number
minSize: number
overlap: number
}
onUploadComplete?: () => void
}
export function UploadModal({
open,
onOpenChange,
knowledgeBaseId,
chunkingConfig,
onUploadComplete,
}: UploadModalProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const fileInputRef = useRef<HTMLInputElement>(null)
const [files, setFiles] = useState<FileWithPreview[]>([])
const [fileError, setFileError] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const { isUploading, uploadProgress, uploadError, uploadFiles, clearError } = useKnowledgeUpload({
workspaceId,
onUploadComplete: () => {
logger.info(`Successfully uploaded ${files.length} files`)
onUploadComplete?.()
handleClose()
},
})
const handleClose = () => {
if (isUploading) return // Prevent closing during upload
setFiles([])
setFileError(null)
clearError()
setIsDragging(false)
onOpenChange(false)
}
const validateFile = (file: File): string | null => {
return validateKnowledgeBaseFile(file)
}
const processFiles = (fileList: FileList | File[]) => {
setFileError(null)
const newFiles: FileWithPreview[] = []
for (const file of Array.from(fileList)) {
const error = validateFile(file)
if (error) {
setFileError(error)
return
}
const fileWithPreview = Object.assign(file, {
preview: URL.createObjectURL(file),
})
newFiles.push(fileWithPreview)
}
setFiles((prev) => [...prev, ...newFiles])
}
const removeFile = (index: number) => {
setFiles((prev) => {
const newFiles = [...prev]
const removedFile = newFiles.splice(index, 1)[0]
if (removedFile.preview) {
URL.revokeObjectURL(removedFile.preview)
}
return newFiles
})
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
processFiles(e.target.files)
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
if (e.dataTransfer.files) {
processFiles(e.dataTransfer.files)
}
}
const handleUpload = async () => {
if (files.length === 0) return
try {
await uploadFiles(files, knowledgeBaseId, {
chunkSize: chunkingConfig?.maxSize || 1024,
minCharactersPerChunk: chunkingConfig?.minSize || 1,
chunkOverlap: chunkingConfig?.overlap || 200,
recipe: 'default',
})
} catch (error) {
logger.error('Error uploading files:', error)
}
}
const getFileIcon = (mimeType: string, filename: string) => {
const IconComponent = getDocumentIcon(mimeType, filename)
return <IconComponent className='h-10 w-8' />
}
return (
<Modal open={open} onOpenChange={handleClose}>
<ModalContent className='max-h-[95vh] sm:max-w-[600px]'>
<ModalHeader>Upload Documents</ModalHeader>
<ModalBody>
<div className='space-y-[12px]'>
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
Select Files
</Label>
{files.length === 0 ? (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed p-8 text-center transition-colors ${
isDragging
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
}`}
>
<input
ref={fileInputRef}
type='file'
accept={ACCEPT_ATTRIBUTE}
onChange={handleFileChange}
className='hidden'
multiple
/>
<div className='space-y-2'>
<p className='font-medium text-[var(--text-primary)] text-sm'>
{isDragging ? 'Drop files here!' : 'Drop files here or click to browse'}
</p>
<p className='text-[var(--text-tertiary)] text-xs'>
Supports PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML, JSON, YAML,
YML (max 100MB each)
</p>
</div>
</div>
) : (
<div className='space-y-2'>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`cursor-pointer rounded-md border border-dashed p-3 text-center transition-colors ${
isDragging
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
}`}
>
<input
ref={fileInputRef}
type='file'
accept={ACCEPT_ATTRIBUTE}
onChange={handleFileChange}
className='hidden'
multiple
/>
<p className='text-[var(--text-primary)] text-sm'>
{isDragging ? 'Drop more files here!' : 'Drop more files or click to browse'}
</p>
</div>
<div className='max-h-80 space-y-2 overflow-auto'>
{files.map((file, index) => {
const fileStatus = uploadProgress.fileStatuses?.[index]
const isCurrentlyUploading = fileStatus?.status === 'uploading'
const isCompleted = fileStatus?.status === 'completed'
const isFailed = fileStatus?.status === 'failed'
return (
<div key={index} className='rounded-md border p-3'>
<div className='flex items-center gap-3'>
{getFileIcon(file.type, file.name)}
<div className='min-w-0 flex-1'>
<div className='flex items-center gap-2'>
{isCurrentlyUploading && (
<Loader2 className='h-4 w-4 animate-spin text-[var(--brand-primary-hex)]' />
)}
{isCompleted && (
<Check className='h-4 w-4 text-[var(--text-success)]' />
)}
{isFailed && <X className='h-4 w-4 text-[var(--text-error)]' />}
<p className='truncate font-medium text-[var(--text-primary)] text-sm'>
{file.name}
</p>
</div>
<div className='flex items-center gap-2'>
<p className='text-[var(--text-tertiary)] text-xs'>
{formatFileSize(file.size)}
</p>
{isCurrentlyUploading && (
<div className='min-w-0 max-w-32 flex-1'>
<Progress value={fileStatus?.progress || 0} className='h-1' />
</div>
)}
</div>
{isFailed && fileStatus?.error && (
<p className='mt-1 text-[var(--text-error)] text-xs'>
{fileStatus.error}
</p>
)}
</div>
<Button
type='button'
variant='ghost'
onClick={() => removeFile(index)}
disabled={isUploading}
className='h-8 w-8 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-error)]'
>
<X className='h-4 w-4' />
</Button>
</div>
</div>
)
})}
</div>
</div>
)}
{/* Show upload error first, then file error only if no upload error */}
{uploadError && (
<div className='rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 px-3 py-2'>
<div className='flex items-start gap-2'>
<AlertCircle className='mt-0.5 h-4 w-4 shrink-0 text-[var(--text-error)]' />
<div className='flex-1 text-[var(--text-error)] text-sm'>
{uploadError.message}
</div>
</div>
</div>
)}
{fileError && !uploadError && (
<div className='rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 px-3 py-2 text-[var(--text-error)] text-sm'>
{fileError}
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={handleClose} disabled={isUploading}>
Cancel
</Button>
<Button
variant='primary'
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
>
{isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted + 1}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Uploading...'
: `Upload ${files.length} file${files.length !== 1 ? 's' : ''}`}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}

Some files were not shown because too many files have changed in this diff Show More