mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 15:38:00 -05:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842ef27ed9 | ||
|
|
f208ff9356 | ||
|
|
31c34b2ea3 | ||
|
|
304cafe698 | ||
|
|
472aff5dd7 | ||
|
|
4d5c574363 | ||
|
|
e7d4afa46b | ||
|
|
f570592ad7 | ||
|
|
e64b1c9fcd | ||
|
|
7c5d625ca5 | ||
|
|
e5cb6e3d0f | ||
|
|
e4ccedc4ff | ||
|
|
d9cb63ce5f | ||
|
|
3468593f84 | ||
|
|
95d5fd9c35 | ||
|
|
becd19bc50 | ||
|
|
c93f6620f6 | ||
|
|
3647a3e38c | ||
|
|
f609b6ea4a | ||
|
|
2be3007d69 | ||
|
|
570b8d61f0 | ||
|
|
7045c4a47b | ||
|
|
5e11e5df91 | ||
|
|
2608f2f12c | ||
|
|
96207d85a7 | ||
|
|
a7fe1d3aea | ||
|
|
3faab2cb01 | ||
|
|
02d9fedf0c | ||
|
|
a8a693f1ff | ||
|
|
e0aade85a6 | ||
|
|
8f0ef58056 | ||
|
|
33ca1483aa | ||
|
|
620ce97056 | ||
|
|
25ac91779b | ||
|
|
d51a756c1b | ||
|
|
3d1feab507 | ||
|
|
98908dbfb9 | ||
|
|
00d9b45a22 | ||
|
|
b5b2855b40 | ||
|
|
a81f3847df | ||
|
|
3058e35edf | ||
|
|
6f3dee867c | ||
|
|
bfa7c919d8 | ||
|
|
e37b01b92c | ||
|
|
7e3e38a6f2 | ||
|
|
1c85fe9a51 | ||
|
|
5f446ad756 | ||
|
|
d99d5fe39c | ||
|
|
949f9287cf | ||
|
|
fca92a7499 | ||
|
|
c25ea5c677 | ||
|
|
dccd9e9ce5 | ||
|
|
b5d9964c48 | ||
|
|
4bd0f31f36 | ||
|
|
f8070f9029 | ||
|
|
bc8947caa6 | ||
|
|
f1111ec16f | ||
|
|
d0767507b2 | ||
|
|
8bd75debc1 | ||
|
|
ad2a375358 | ||
|
|
de91dc97a9 | ||
|
|
31ed712378 |
13
.github/workflows/docs-embeddings.yml
vendored
13
.github/workflows/docs-embeddings.yml
vendored
@@ -24,8 +24,19 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Process docs embeddings
|
||||
working-directory: ./apps/sim
|
||||
|
||||
24
.github/workflows/i18n.yml
vendored
24
.github/workflows/i18n.yml
vendored
@@ -28,6 +28,17 @@ jobs:
|
||||
with:
|
||||
bun-version: 1.2.22
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Run Lingo.dev translations
|
||||
env:
|
||||
LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }}
|
||||
@@ -117,10 +128,21 @@ jobs:
|
||||
with:
|
||||
bun-version: 1.2.22
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd apps/docs
|
||||
bun install
|
||||
bun install --frozen-lockfile
|
||||
|
||||
- name: Build documentation to verify translations
|
||||
run: |
|
||||
|
||||
13
.github/workflows/migrations.yml
vendored
13
.github/workflows/migrations.yml
vendored
@@ -18,8 +18,19 @@ jobs:
|
||||
with:
|
||||
bun-version: 1.2.22
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Apply migrations
|
||||
working-directory: ./packages/db
|
||||
|
||||
13
.github/workflows/publish-cli.yml
vendored
13
.github/workflows/publish-cli.yml
vendored
@@ -24,9 +24,20 @@ jobs:
|
||||
node-version: '18'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: packages/cli
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build package
|
||||
working-directory: packages/cli
|
||||
|
||||
13
.github/workflows/publish-ts-sdk.yml
vendored
13
.github/workflows/publish-ts-sdk.yml
vendored
@@ -24,8 +24,19 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
working-directory: packages/ts-sdk
|
||||
|
||||
11
.github/workflows/test-build.yml
vendored
11
.github/workflows/test-build.yml
vendored
@@ -23,6 +23,17 @@ jobs:
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
|
||||
13
.github/workflows/trigger-deploy.yml
vendored
13
.github/workflows/trigger-deploy.yml
vendored
@@ -29,8 +29,19 @@ jobs:
|
||||
with:
|
||||
bun-version: 1.2.22
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.bun/install/cache
|
||||
node_modules
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Deploy to Trigger.dev (Staging)
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
|
||||
@@ -198,15 +198,17 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
component: <CustomFooter />,
|
||||
}}
|
||||
>
|
||||
<div className='relative'>
|
||||
<div className='relative mt-6 sm:mt-0'>
|
||||
<div className='absolute top-1 right-0 flex items-center gap-2'>
|
||||
<CopyPageButton
|
||||
content={`# ${page.data.title}
|
||||
<div className='hidden sm:flex'>
|
||||
<CopyPageButton
|
||||
content={`# ${page.data.title}
|
||||
|
||||
${page.data.description || ''}
|
||||
|
||||
${page.data.content || ''}`}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
|
||||
</div>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function SidebarFolder({
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className='rounded p-1 transition-colors hover:bg-gray-100/60 dark:hover:bg-gray-800/40'
|
||||
className='cursor-pointer rounded p-1 transition-colors hover:bg-gray-100/60 dark:hover:bg-gray-800/40'
|
||||
aria-label={open ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
<ChevronRight
|
||||
@@ -84,7 +84,7 @@ export function SidebarFolder({
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-left font-medium text-[13px] leading-tight transition-colors',
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-md px-2.5 py-1.5 text-left font-medium text-[13px] leading-tight transition-colors',
|
||||
'hover:bg-gray-100/60 dark:hover:bg-gray-800/40',
|
||||
'text-gray-800 dark:text-gray-200'
|
||||
)}
|
||||
|
||||
@@ -1802,38 +1802,38 @@ export function StripeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M360 78.0002C360 52.4002 347.6 32.2002 323.9 32.2002C300.1 32.2002 285.7 52.4002 285.7 77.8002C285.7 107.9 302.7 123.1 327.1 123.1C339 123.1 348 120.4 354.8 116.6V96.6002C348 100 340.2 102.1 330.3 102.1C320.6 102.1 312 98.7002 310.9 86.9002H359.8C359.8 85.6002 360 80.4002 360 78.0002ZM310.6 68.5002C310.6 57.2002 317.5 52.5002 323.8 52.5002C329.9 52.5002 336.4 57.2002 336.4 68.5002H310.6Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M247.1 32.2002C237.3 32.2002 231 36.8002 227.5 40.0002L226.2 33.8002H204.2V150.4L229.2 145.1L229.3 116.8C232.9 119.4 238.2 123.1 247 123.1C264.9 123.1 281.2 108.7 281.2 77.0002C281.1 48.0002 264.6 32.2002 247.1 32.2002ZM241.1 101.1C235.2 101.1 231.7 99.0002 229.3 96.4002L229.2 59.3002C231.8 56.4002 235.4 54.4002 241.1 54.4002C250.2 54.4002 256.5 64.6002 256.5 77.7002C256.5 91.1002 250.3 101.1 241.1 101.1Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M169.8 26.3001L194.9 20.9001V0.600098L169.8 5.9001V26.3001Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path d='M194.9 33.9001H169.8V121.4H194.9V33.9001Z' fill='white' />
|
||||
<path d='M194.9 33.9001H169.8V121.4H194.9V33.9001Z' fill='currentColor' />
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M142.9 41.3001L141.3 33.9001H119.7V121.4H144.7V62.1001C150.6 54.4001 160.6 55.8001 163.7 56.9001V33.9001C160.5 32.7001 148.8 30.5001 142.9 41.3001Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M92.8999 12.2002L68.4999 17.4002L68.3999 97.5002C68.3999 112.3 79.4999 123.2 94.2999 123.2C102.5 123.2 108.5 121.7 111.8 119.9V99.6002C108.6 100.9 92.7999 105.5 92.7999 90.7002V55.2002H111.8V33.9002H92.7999L92.8999 12.2002Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M25.3 59.3002C25.3 55.4002 28.5 53.9002 33.8 53.9002C41.4 53.9002 51 56.2002 58.6 60.3002V36.8002C50.3 33.5002 42.1 32.2002 33.8 32.2002C13.5 32.2002 0 42.8002 0 60.5002C0 88.1002 38 83.7002 38 95.6002C38 100.2 34 101.7 28.4 101.7C20.1 101.7 9.5 98.3002 1.1 93.7002V117.5C10.4 121.5 19.8 123.2 28.4 123.2C49.2 123.2 63.5 112.9 63.5 95.0002C63.4 65.2002 25.3 70.5002 25.3 59.3002Z'
|
||||
fill='white'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -4032,3 +4032,79 @@ export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Neo4jIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 128 128' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M63.333 32.567c-5.2.866-9.566 3-12.833 6.266-3.867 3.867-5.833 8.5-6.5 15.367-.3 3.133-.467 15.467-.2 15.467.067 0 .7-.234 1.4-.534 1.633-.7 5.167-.7 7-.033l1.4.5.167-8.033c.166-8.567.366-9.867 1.966-13.067 1.1-2.133 3.767-4.633 6.034-5.667 2.6-1.2 6.4-1.666 9.333-1.2 6.267 1.034 10 4.434 11.567 10.5.633 2.434.666 3.7.666 17.1v14.434H93.4L93.233 67.9c-.1-14.9-.166-15.9-.866-18.567-1.9-7.4-6.5-12.766-12.934-15.2-3.433-1.3-6.7-1.8-11.2-1.766-2.233.033-4.433.133-4.9.2z'
|
||||
fill='#000'
|
||||
/>
|
||||
<path
|
||||
d='M22.733 57.2c-2.866 1.433-4.4 4-4.4 7.467 0 1.1.2 2.5.467 3.133.633 1.567 2.433 3.467 4 4.3 1.9 1 5.5 1 7.367.033l1.366-.7 4.267 2.9 4.267 2.934V81.7L35.8 84.633l-4.3 2.934-1.1-.667c-1.6-.933-4.7-1.133-6.6-.4-2 .767-4.067 2.6-4.833 4.333-.834 1.767-.834 5.234 0 7 .7 1.567 2.333 3.3 3.8 4.067.6.3 2.033.6 3.233.7 2.8.2 5.167-.733 6.867-2.733 1.366-1.6 2.266-4.4 2.033-6.334l-.167-1.366 4.3-2.9 4.3-2.9 1.534.7c2.333 1 5.8.766 8-.567 2.4-1.5 3.6-3.633 3.733-6.633.1-2.1 0-2.567-.833-4.2-2.167-4.134-7-5.7-11.134-3.634l-1.233.6-4.233-2.9-4.234-2.9-.1-2.333c-.066-2.8-.866-4.6-2.833-6.233-2.5-2.134-6.233-2.567-9.267-1.067z'
|
||||
fill='#018BFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CalendlyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 512 512'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
strokeMiterlimit='2'
|
||||
>
|
||||
<g fillRule='nonzero'>
|
||||
<path
|
||||
d='M346.955 330.224c-15.875 14.088-35.7 31.619-71.647 31.619h-21.495c-26.012 0-49.672-9.455-66.607-26.593-16.543-16.747-25.649-39.665-25.649-64.545v-29.41c0-24.88 9.106-47.799 25.65-64.545 16.934-17.138 40.594-26.579 66.606-26.579h21.495c35.99 0 55.772 17.516 71.647 31.604 16.484 14.524 30.703 27.218 68.625 27.218a109.162 109.162 0 0017.269-1.38l-.13-.334a129.909 129.909 0 00-7.974-16.382L399.4 146.99c-23.232-40.234-66.304-65.098-112.763-65.096h-50.703c-46.46-.002-89.531 24.862-112.764 65.096l-25.344 43.906c-23.224 40.238-23.224 89.968 0 130.206l25.344 43.906c23.233 40.234 66.305 65.098 112.764 65.096h50.703c46.459.002 89.53-24.862 112.763-65.096l25.345-43.833a129.909 129.909 0 007.973-16.383l.13-.32a107.491 107.491 0 00-17.268-1.452c-37.922 0-52.14 12.621-68.625 27.218'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M275.308 176.823h-21.495c-39.592 0-65.605 28.278-65.605 64.471v29.411c0 36.194 26.013 64.472 65.605 64.472h21.495c57.69 0 53.158-58.822 140.272-58.822 8.254-.009 16.49.75 24.603 2.266a130.047 130.047 0 000-45.242 134.431 134.431 0 01-24.603 2.266c-87.143 0-82.583-58.822-140.272-58.822'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M490.233 300.116a121.451 121.451 0 00-50.035-21.51v.436a130.296 130.296 0 01-7.262 25.344 95.25 95.25 0 0141.364 17.037c0 .116-.072.261-.116.392-28.788 93.217-115.55 157.228-213.112 157.228-122.358 0-223.044-100.685-223.044-223.043S138.714 32.956 261.072 32.956c97.561 0 184.324 64.012 213.112 157.229 0 .13.073.276.116.392a95.073 95.073 0 01-41.364 17.022 131.112 131.112 0 017.262 25.373 3.166 3.166 0 000 .407 121.415 121.415 0 0050.035-21.495c14.262-10.56 11.503-22.483 9.339-29.542C467.34 77.803 370.064 6 260.67 6c-137.147 0-250 112.854-250 250 0 137.146 112.853 250 250 250 109.394 0 206.67-71.803 238.902-176.342 2.164-7.059 4.923-18.983-9.34-29.542'
|
||||
fill='#006bff'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
<path
|
||||
d='M432.849 207.599a107.491 107.491 0 01-17.269 1.452c-37.922 0-52.14-12.62-68.61-27.217-15.89-14.089-35.672-31.619-71.662-31.619h-21.495c-26.027 0-49.672 9.455-66.607 26.593-16.543 16.746-25.649 39.665-25.649 64.545v29.41c0 24.88 9.106 47.799 25.65 64.545 16.934 17.138 40.579 26.578 66.606 26.578h21.495c35.99 0 55.772-17.515 71.661-31.604 16.47-14.524 30.69-27.217 68.611-27.217 5.783.001 11.558.463 17.269 1.38a129.303 129.303 0 007.262-25.345c.009-.145.009-.29 0-.436a134.301 134.301 0 00-24.604-2.25c-87.143 0-82.583 58.836-140.271 58.836H253.74c-39.592 0-65.604-28.293-65.604-64.487v-29.469c0-36.193 26.012-64.471 65.604-64.471h21.496c57.688 0 53.157 58.807 140.271 58.807 8.254.015 16.49-.74 24.604-2.251v-.407a131.112 131.112 0 00-7.262-25.373'
|
||||
fill='#0ae8f0'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AudioWaveformIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M2 10v3' />
|
||||
<path d='M6 6v11' />
|
||||
<path d='M10 3v18' />
|
||||
<path d='M14 8v7' />
|
||||
<path d='M18 5v13' />
|
||||
<path d='M22 10v3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function CodeBlock(props: React.ComponentProps<typeof FumadocsCodeBlock>)
|
||||
if (pre) handleCopy(pre.textContent || '')
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-md p-2 transition-all',
|
||||
'cursor-pointer rounded-md p-2 transition-all',
|
||||
'border border-border bg-background/80 hover:bg-muted',
|
||||
'backdrop-blur-sm'
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function CopyPageButton({ content }: CopyPageButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className='flex 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'
|
||||
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'}
|
||||
>
|
||||
{copied ? (
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AudioWaveformIcon,
|
||||
BrainIcon,
|
||||
BrowserUseIcon,
|
||||
CalendlyIcon,
|
||||
ClayIcon,
|
||||
ConfluenceIcon,
|
||||
DiscordIcon,
|
||||
@@ -44,6 +46,7 @@ import {
|
||||
MistralIcon,
|
||||
MongoDBIcon,
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
@@ -98,6 +101,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
telegram: TelegramIcon,
|
||||
tavily: TavilyIcon,
|
||||
supabase: SupabaseIcon,
|
||||
stt: AudioWaveformIcon,
|
||||
stripe: StripeIcon,
|
||||
stagehand_agent: StagehandIcon,
|
||||
stagehand: StagehandIcon,
|
||||
@@ -118,6 +122,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
openai: OpenAIIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
notion: NotionIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
mysql: MySQLIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
mistral_parse: MistralIcon,
|
||||
@@ -151,6 +156,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
discord: DiscordIcon,
|
||||
confluence: ConfluenceIcon,
|
||||
clay: ClayIcon,
|
||||
calendly: CalendlyIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
asana: AsanaIcon,
|
||||
arxiv: ArxivIcon,
|
||||
|
||||
@@ -82,7 +82,7 @@ export function LanguageDropdown() {
|
||||
aria-haspopup='listbox'
|
||||
aria-expanded={isOpen}
|
||||
aria-controls='language-menu'
|
||||
className='flex items-center gap-1.5 rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
||||
className='flex cursor-pointer items-center gap-1.5 rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
||||
style={{
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
@@ -110,7 +110,7 @@ export function LanguageDropdown() {
|
||||
}}
|
||||
role='option'
|
||||
aria-selected={currentLang === code}
|
||||
className={`flex w-full items-center gap-3 px-3 py-3 text-base transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-muted/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring md:gap-2 md:px-2.5 md:py-2 md:text-sm ${
|
||||
className={`flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-base transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-muted/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring md:gap-2 md:px-2.5 md:py-2 md:text-sm ${
|
||||
currentLang === code ? 'bg-muted/60 font-medium text-primary' : 'text-foreground'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SearchTrigger() {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-10 w-[460px] items-center gap-2 rounded-xl border border-border/50 px-3 py-2 text-sm backdrop-blur-xl transition-colors hover:border-border'
|
||||
className='flex h-10 w-[460px] cursor-pointer items-center gap-2 rounded-xl border border-border/50 px-3 py-2 text-sm backdrop-blur-xl transition-colors hover:border-border'
|
||||
style={{
|
||||
backgroundColor: 'hsla(0, 0%, 5%, 0.85)',
|
||||
backdropFilter: 'blur(33px) saturate(180%)',
|
||||
|
||||
@@ -14,7 +14,7 @@ export function ThemeToggle() {
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button className='flex items-center justify-center rounded-md p-1 text-muted-foreground'>
|
||||
<button className='flex cursor-pointer items-center justify-center rounded-md p-1 text-muted-foreground'>
|
||||
<Moon className='h-4 w-4' />
|
||||
</button>
|
||||
)
|
||||
@@ -23,7 +23,7 @@ export function ThemeToggle() {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className='flex items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:text-foreground'
|
||||
className='flex cursor-pointer items-center justify-center rounded-md p-1 text-muted-foreground transition-colors hover:text-foreground'
|
||||
aria-label='Toggle theme'
|
||||
>
|
||||
{theme === 'dark' ? <Moon className='h-4 w-4' /> : <Sun className='h-4 w-4' />}
|
||||
|
||||
@@ -42,10 +42,10 @@ Der Benutzer-Prompt stellt die primären Eingabedaten für die Inferenzverarbeit
|
||||
|
||||
Der Agent-Block unterstützt mehrere LLM-Anbieter über eine einheitliche Inferenzschnittstelle. Verfügbare Modelle umfassen:
|
||||
|
||||
- **OpenAI**: GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **OpenAI**: GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 4.5 Sonnet, Claude Opus 4.1
|
||||
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Andere Anbieter**: Groq, Cerebras, xAI, DeepSeek
|
||||
- **Andere Anbieter**: Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
|
||||
- **Lokale Modelle**: Ollama-kompatible Modelle
|
||||
|
||||
### Temperatur
|
||||
|
||||
172
apps/docs/content/docs/de/tools/calendly.mdx
Normal file
172
apps/docs/content/docs/de/tools/calendly.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Verwalte Calendly-Terminplanung und Ereignisse
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/) ist eine beliebte Plattform zur Terminplanungsautomatisierung, die Ihnen hilft, Meetings, Events und Termine mühelos zu buchen. Mit Calendly können Teams und Einzelpersonen die Terminplanung optimieren, den E-Mail-Austausch reduzieren und Aufgaben rund um Veranstaltungen automatisieren.
|
||||
|
||||
Mit der Sim Calendly-Integration können Ihre Agenten:
|
||||
|
||||
- **Informationen über Ihr Konto und geplante Ereignisse abrufen**: Verwenden Sie Tools, um Benutzerinformationen, Ereignistypen und geplante Ereignisse für Analysen oder Automatisierungen abzurufen.
|
||||
- **Ereignistypen und Terminplanung verwalten**: Greifen Sie auf verfügbare Ereignistypen für Benutzer oder Organisationen zu und listen Sie diese auf, rufen Sie Details zu bestimmten Ereignistypen ab und überwachen Sie geplante Meetings und Teilnehmerdaten.
|
||||
- **Automatisieren Sie Follow-ups und Workflows**: Wenn Benutzer Meetings planen, umplanen oder stornieren, können Sim-Agenten automatisch entsprechende Workflows auslösen – wie das Senden von Erinnerungen, das Aktualisieren von CRMs oder das Benachrichtigen von Teilnehmern.
|
||||
- **Einfache Integration über Webhooks**: Richten Sie Sim-Workflows ein, um auf Calendly-Webhook-Ereignisse in Echtzeit zu reagieren, einschließlich wenn Eingeladene Termine planen, stornieren oder mit Routing-Formularen interagieren.
|
||||
|
||||
Ob Sie die Meeting-Vorbereitung automatisieren, Einladungen verwalten oder benutzerdefinierte Workflows als Reaktion auf Planungsaktivitäten ausführen möchten – die Calendly-Tools in Sim bieten Ihnen flexiblen und sicheren Zugriff. Erschließen Sie neue Automatisierungsmöglichkeiten, indem Sie sofort auf Planungsänderungen reagieren – und optimieren Sie so die Abläufe und die Kommunikation Ihres Teams.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Nutzungsanweisungen
|
||||
|
||||
Integrieren Sie Calendly in Ihren Workflow. Verwalten Sie Ereignistypen, geplante Ereignisse, Eingeladene und Webhooks. Kann auch Workflows basierend auf Calendly-Webhook-Ereignissen auslösen (Eingeladener hat Termin vereinbart, Eingeladener hat storniert, Routing-Formular wurde eingereicht). Erfordert einen persönlichen Zugriffstoken.
|
||||
|
||||
## Tools
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
Informationen über den aktuell authentifizierten Calendly-Benutzer abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönlicher Zugriffstoken |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Informationen zum aktuellen Benutzer |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
Eine Liste aller Ereignistypen für einen Benutzer oder eine Organisation abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `user` | string | Nein | Nur Ereignistypen zurückgeben, die zu diesem Benutzer gehören \(URI-Format\) |
|
||||
| `organization` | string | Nein | Nur Ereignistypen zurückgeben, die zu dieser Organisation gehören \(URI-Format\) |
|
||||
| `count` | number | Nein | Anzahl der Ergebnisse pro Seite \(Standard: 20, max: 100\) |
|
||||
| `pageToken` | string | Nein | Seitentoken für Paginierung |
|
||||
| `sort` | string | Nein | Sortierreihenfolge für Ergebnisse \(z.B. "name:asc", "name:desc"\) |
|
||||
| `active` | boolean | Nein | Bei true werden nur aktive Ereignistypen angezeigt. Bei false oder nicht ausgewählt werden alle Ereignistypen angezeigt \(sowohl aktive als auch inaktive\). |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array von Ereignistyp-Objekten |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
Detaillierte Informationen über einen bestimmten Ereignistyp abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `eventTypeUuid` | string | Ja | Ereignistyp-UUID \(kann vollständige URI oder nur die UUID sein\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Details zum Ereignistyp |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
Eine Liste geplanter Ereignisse für einen Benutzer oder eine Organisation abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `user` | string | Nein | Gibt Ereignisse zurück, die zu diesem Benutzer gehören \(URI-Format\). Entweder "user" oder "organization" muss angegeben werden. |
|
||||
| `organization` | string | Nein | Gibt Ereignisse zurück, die zu dieser Organisation gehören \(URI-Format\). Entweder "user" oder "organization" muss angegeben werden. |
|
||||
| `invitee_email` | string | Nein | Gibt Ereignisse zurück, bei denen der Eingeladene diese E-Mail hat |
|
||||
| `count` | number | Nein | Anzahl der Ergebnisse pro Seite \(Standard: 20, max: 100\) |
|
||||
| `max_start_time` | string | Nein | Gibt Ereignisse mit Startzeit vor diesem Zeitpunkt zurück \(ISO 8601-Format\) |
|
||||
| `min_start_time` | string | Nein | Gibt Ereignisse mit Startzeit nach diesem Zeitpunkt zurück \(ISO 8601-Format\) |
|
||||
| `pageToken` | string | Nein | Seitentoken für Paginierung |
|
||||
| `sort` | string | Nein | Sortierreihenfolge für Ergebnisse \(z.B. "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | Nein | Nach Status filtern \("active" oder "canceled"\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array von geplanten Ereignisobjekten |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
Detaillierte Informationen über ein bestimmtes geplantes Ereignis abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `eventUuid` | string | Ja | UUID des geplanten Ereignisses \(kann vollständige URI oder nur die UUID sein\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Details zum geplanten Ereignis |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
Eine Liste der Eingeladenen für ein geplantes Ereignis abrufen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `eventUuid` | string | Ja | UUID des geplanten Ereignisses \(kann vollständige URI oder nur die UUID sein\) |
|
||||
| `count` | number | Nein | Anzahl der Ergebnisse pro Seite \(Standard: 20, max: 100\) |
|
||||
| `email` | string | Nein | Eingeladene nach E-Mail-Adresse filtern |
|
||||
| `pageToken` | string | Nein | Seitentoken für Paginierung |
|
||||
| `sort` | string | Nein | Sortierreihenfolge für Ergebnisse \(z.B. "created_at:asc", "created_at:desc"\) |
|
||||
| `status` | string | Nein | Nach Status filtern \("active" oder "canceled"\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array von Eingeladenen-Objekten |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
Ein geplantes Ereignis stornieren
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Ja | Calendly persönliches Zugriffstoken |
|
||||
| `eventUuid` | string | Ja | UUID des zu stornierenden geplanten Ereignisses \(kann vollständige URI oder nur die UUID sein\) |
|
||||
| `reason` | string | Nein | Grund für die Stornierung \(wird an Eingeladene gesendet\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Stornierungsdetails |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
- Typ: `calendly`
|
||||
@@ -24,9 +24,11 @@ Füge eine neue Erinnerung zur Datenbank hinzu oder ergänze bestehende Erinneru
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Ja | Kennung für die Erinnerung. Wenn bereits eine Erinnerung mit dieser ID existiert, werden die neuen Daten hinzugefügt. |
|
||||
| `role` | string | Ja | Rolle für Agent-Erinnerung \(user, assistant oder system\) |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn bereits eine Erinnerung mit dieser conversationId für diesen Block existiert, wird die neue Nachricht angehängt. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `role` | string | Ja | Rolle für Agent-Erinnerung (user, assistant oder system) |
|
||||
| `content` | string | Ja | Inhalt für Agent-Erinnerung |
|
||||
| `blockId` | string | Nein | Optionale Block-ID. Wenn nicht angegeben, wird die aktuelle Block-ID aus dem Ausführungskontext verwendet oder standardmäßig "default" gesetzt. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -38,22 +40,25 @@ Füge eine neue Erinnerung zur Datenbank hinzu oder ergänze bestehende Erinneru
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Einen bestimmten Speicher anhand seiner ID abrufen
|
||||
Erinnerungen nach conversationId, blockId, blockName oder einer Kombination abrufen. Gibt alle übereinstimmenden Erinnerungen zurück.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Ja | Kennung für den abzurufenden Speicher |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn allein angegeben, werden alle Erinnerungen für diese Konversation über alle Blöcke hinweg zurückgegeben. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `blockId` | string | Nein | Block-Kennung. Wenn allein angegeben, werden alle Erinnerungen für diesen Block über alle Konversationen hinweg zurückgegeben. Wenn mit conversationId angegeben, werden Erinnerungen für diese spezifische Konversation in diesem Block zurückgegeben. |
|
||||
| `blockName` | string | Nein | Blockname. Alternative zu blockId. Wenn allein angegeben, werden alle Erinnerungen für Blöcke mit diesem Namen zurückgegeben. Wenn mit conversationId angegeben, werden Erinnerungen für diese Konversation in Blöcken mit diesem Namen zurückgegeben. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob der Speicher erfolgreich abgerufen wurde |
|
||||
| `memories` | array | Array von Speicherdaten für die angeforderte ID |
|
||||
| `success` | boolean | Ob die Erinnerung erfolgreich abgerufen wurde |
|
||||
| `memories` | array | Array von Speicherobjekten mit conversationId, blockId, blockName und data-Feldern |
|
||||
| `message` | string | Erfolgs- oder Fehlermeldung |
|
||||
| `error` | string | Fehlermeldung, falls der Vorgang fehlgeschlagen ist |
|
||||
| `error` | string | Fehlermeldung, wenn der Vorgang fehlgeschlagen ist |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -68,20 +73,23 @@ Alle Speicher aus der Datenbank abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob alle Speicher erfolgreich abgerufen wurden |
|
||||
| `memories` | array | Array aller Speicherobjekte mit Schlüsseln, Typen und Daten |
|
||||
| `success` | boolean | Ob alle Erinnerungen erfolgreich abgerufen wurden |
|
||||
| `memories` | array | Array aller Speicherobjekte mit key, conversationId, blockId, blockName und data-Feldern |
|
||||
| `message` | string | Erfolgs- oder Fehlermeldung |
|
||||
| `error` | string | Fehlermeldung, falls der Vorgang fehlgeschlagen ist |
|
||||
| `error` | string | Fehlermeldung, wenn der Vorgang fehlgeschlagen ist |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Eine bestimmte Erinnerung anhand ihrer ID löschen
|
||||
Löschen von Erinnerungen nach conversationId, blockId, blockName oder einer Kombination davon. Unterstützt Massenlöschung.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Ja | Kennung für die zu löschende Erinnerung |
|
||||
| `conversationId` | string | Nein | Konversationskennung (z.B. user-123, session-abc). Wenn allein angegeben, werden alle Erinnerungen für diese Konversation über alle Blöcke hinweg gelöscht. |
|
||||
| `id` | string | Nein | Legacy-Parameter für die Konversationskennung. Verwenden Sie stattdessen conversationId. Für Abwärtskompatibilität bereitgestellt. |
|
||||
| `blockId` | string | Nein | Block-Kennung. Wenn allein angegeben, werden alle Erinnerungen für diesen Block über alle Konversationen hinweg gelöscht. Wenn mit conversationId angegeben, werden Erinnerungen für diese spezifische Konversation in diesem Block gelöscht. |
|
||||
| `blockName` | string | Nein | Blockname. Alternative zu blockId. Wenn allein angegeben, werden alle Erinnerungen für Blöcke mit diesem Namen gelöscht. Wenn mit conversationId angegeben, werden Erinnerungen für diese Konversation in Blöcken mit diesem Namen gelöscht. |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ In Sim bietet die Microsoft Excel-Integration nahtlosen Zugriff auf Tabellenkalk
|
||||
|
||||
## Nutzungsanweisungen
|
||||
|
||||
Integrieren Sie Microsoft Excel in den Workflow. Kann Tabellen lesen, schreiben, aktualisieren und ergänzen. Erfordert OAuth.
|
||||
Integrieren Sie Microsoft Excel in den Workflow. Kann lesen, schreiben, aktualisieren, zu Tabellen hinzufügen und neue Arbeitsblätter erstellen.
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -91,6 +91,23 @@ Neue Zeilen zu einer Microsoft Excel-Tabelle hinzufügen
|
||||
| `values` | array | Array von Zeilen, die zur Tabelle hinzugefügt wurden |
|
||||
| `metadata` | object | Metadaten der Tabellenkalkulation |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
Ein neues Arbeitsblatt (Tabellenblatt) in einer Microsoft Excel-Arbeitsmappe erstellen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Ja | Die ID der Excel-Arbeitsmappe, zu der das Arbeitsblatt hinzugefügt werden soll |
|
||||
| `worksheetName` | string | Ja | Der Name des neuen Arbeitsblatts. Muss innerhalb der Arbeitsmappe eindeutig sein und darf 31 Zeichen nicht überschreiten |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | object | Details des neu erstellten Arbeitsblatts |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
|
||||
172
apps/docs/content/docs/de/tools/neo4j.mdx
Normal file
172
apps/docs/content/docs/de/tools/neo4j.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Verbindung zur Neo4j-Graphdatenbank
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Nutzungsanleitung
|
||||
|
||||
Integrieren Sie die Neo4j-Graphdatenbank in den Workflow. Kann Knoten und Beziehungen abfragen, erstellen, zusammenführen, aktualisieren und löschen.
|
||||
|
||||
## Tools
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
Führen Sie MATCH-Abfragen aus, um Knoten und Beziehungen aus der Neo4j-Graphdatenbank zu lesen. Für beste Leistung und zur Vermeidung großer Ergebnismengen, fügen Sie LIMIT in Ihre Abfrage ein (z.B.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Ja | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Ja | Neo4j-Server-Port \(Standard: 7687 für Bolt-Protokoll\) |
|
||||
| `database` | string | Ja | Datenbankname für die Verbindung |
|
||||
| `username` | string | Ja | Neo4j-Benutzername |
|
||||
| `password` | string | Ja | Neo4j-Passwort |
|
||||
| `encryption` | string | Nein | Verbindungsverschlüsselungsmodus \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Ja | Auszuführende Cypher-Abfrage \(typischerweise MATCH-Anweisungen\) |
|
||||
| `parameters` | object | Nein | Parameter für die Cypher-Abfrage als JSON-Objekt. Verwenden Sie diese für dynamische Werte einschließlich LIMIT \(z.B. query: "MATCH \(n\) RETURN n LIMIT $limit", parameters: \{limit: 100\}\). |
|
||||
| `parameters` | string | Nein | Keine Beschreibung |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `records` | array | Array von Datensätzen, die von der Abfrage zurückgegeben wurden |
|
||||
| `recordCount` | number | Anzahl der zurückgegebenen Datensätze |
|
||||
| `summary` | json | Zusammenfassung der Abfrageausführung mit Zeitangaben und Zählern |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
Führe CREATE-Anweisungen aus, um neue Knoten und Beziehungen zur Neo4j-Graphdatenbank hinzuzufügen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Ja | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Ja | Neo4j-Server-Port \(Standard: 7687 für Bolt-Protokoll\) |
|
||||
| `database` | string | Ja | Datenbankname, zu dem verbunden werden soll |
|
||||
| `username` | string | Ja | Neo4j-Benutzername |
|
||||
| `password` | string | Ja | Neo4j-Passwort |
|
||||
| `encryption` | string | Nein | Verbindungsverschlüsselungsmodus \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Ja | Auszuführende Cypher CREATE-Anweisung |
|
||||
| `parameters` | object | Nein | Parameter für die Cypher-Abfrage als JSON-Objekt |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `summary` | json | Erstellungszusammenfassung mit Zählern für erstellte Knoten und Beziehungen |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
Führe MERGE-Anweisungen aus, um Knoten und Beziehungen in Neo4j zu finden oder zu erstellen (Upsert-Operation)
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Ja | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Ja | Neo4j-Server-Port \(Standard: 7687 für Bolt-Protokoll\) |
|
||||
| `database` | string | Ja | Datenbankname, zu dem verbunden werden soll |
|
||||
| `username` | string | Ja | Neo4j-Benutzername |
|
||||
| `password` | string | Ja | Neo4j-Passwort |
|
||||
| `encryption` | string | Nein | Verbindungsverschlüsselungsmodus \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Ja | Auszuführende Cypher MERGE-Anweisung |
|
||||
| `parameters` | object | Nein | Parameter für die Cypher-Abfrage als JSON-Objekt |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `summary` | json | Zusammenfassung der Zusammenführung mit Zählern für erstellte oder zugeordnete Knoten/Beziehungen |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Führt SET-Anweisungen aus, um Eigenschaften vorhandener Knoten und Beziehungen in Neo4j zu aktualisieren
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Yes | Neo4j-Server-Port \(Standard: 7687 für Bolt-Protokoll\) |
|
||||
| `database` | string | Yes | Datenbankname, zu dem eine Verbindung hergestellt werden soll |
|
||||
| `username` | string | Yes | Neo4j-Benutzername |
|
||||
| `password` | string | Yes | Neo4j-Passwort |
|
||||
| `encryption` | string | No | Verbindungsverschlüsselungsmodus \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher-Abfrage mit MATCH- und SET-Anweisungen zum Aktualisieren von Eigenschaften |
|
||||
| `parameters` | object | No | Parameter für die Cypher-Abfrage als JSON-Objekt |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `summary` | json | Aktualisierungszusammenfassung mit Zählern für gesetzte Eigenschaften |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Führt DELETE- oder DETACH DELETE-Anweisungen aus, um Knoten und Beziehungen aus Neo4j zu entfernen
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Yes | Neo4j-Server-Port \(Standard: 7687 für Bolt-Protokoll\) |
|
||||
| `database` | string | Yes | Datenbankname, zu dem eine Verbindung hergestellt werden soll |
|
||||
| `username` | string | Yes | Neo4j-Benutzername |
|
||||
| `password` | string | Yes | Neo4j-Passwort |
|
||||
| `encryption` | string | No | Verbindungsverschlüsselungsmodus \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher-Abfrage mit MATCH- und DELETE/DETACH DELETE-Anweisungen |
|
||||
| `parameters` | object | No | Parameter für die Cypher-Abfrage als JSON-Objekt |
|
||||
| `detach` | boolean | No | Ob DETACH DELETE verwendet werden soll, um Beziehungen zu entfernen, bevor Knoten gelöscht werden |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `summary` | json | Löschzusammenfassung mit Zählern für gelöschte Knoten und Beziehungen |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
Führt beliebige Cypher-Abfragen auf der Neo4j-Graphdatenbank für komplexe Operationen aus
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Ja | Neo4j-Server-Hostname oder IP-Adresse |
|
||||
| `port` | number | Ja | Neo4j-Server-Port (Standard: 7687 für Bolt-Protokoll) |
|
||||
| `database` | string | Ja | Datenbankname für die Verbindung |
|
||||
| `username` | string | Ja | Neo4j-Benutzername |
|
||||
| `password` | string | Ja | Neo4j-Passwort |
|
||||
| `encryption` | string | Nein | Verbindungsverschlüsselungsmodus (enabled, disabled) |
|
||||
| `cypherQuery` | string | Ja | Auszuführende Cypher-Abfrage (jede gültige Cypher-Anweisung) |
|
||||
| `parameters` | object | Nein | Parameter für die Cypher-Abfrage als JSON-Objekt |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Statusmeldung der Operation |
|
||||
| `records` | array | Array von Datensätzen, die von der Abfrage zurückgegeben wurden |
|
||||
| `recordCount` | number | Anzahl der zurückgegebenen Datensätze |
|
||||
| `summary` | json | Ausführungszusammenfassung mit Zeiterfassung und Zählern |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
- Typ: `neo4j`
|
||||
117
apps/docs/content/docs/de/tools/stt.mdx
Normal file
117
apps/docs/content/docs/de/tools/stt.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Speech-to-Text
|
||||
description: Konvertiere Sprache in Text mit KI
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Transkribiere Sprache zu Text mit modernsten KI-Modellen führender Anbieter. Die Sim Speech-to-Text (STT)-Tools ermöglichen es dir, Audio- und Videodateien in präzise Transkripte umzuwandeln, mit Unterstützung für mehrere Sprachen, Zeitstempel und optionaler Übersetzung.
|
||||
|
||||
Unterstützte Anbieter:
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)**: Fortschrittliches Open-Source-STT-Modell von OpenAI. Unterstützt Modelle wie `whisper-1` und verarbeitet eine Vielzahl von Sprachen und Audioformaten.
|
||||
- **[Deepgram](https://deepgram.com/)**: Echtzeit- und Batch-STT-API mit Deep-Learning-Modellen wie `nova-3`, `nova-2` und `whisper-large`. Bietet Funktionen wie Sprechererkennung, Intentionserkennung und branchenspezifische Anpassungen.
|
||||
- **[ElevenLabs](https://elevenlabs.io/)**: Bekannt für hochwertige Sprach-KI, bietet ElevenLabs STT-Modelle mit Fokus auf Genauigkeit und natürlichem Sprachverständnis für zahlreiche Sprachen und Dialekte.
|
||||
|
||||
Wähle den Anbieter und das Modell, das am besten zu deiner Aufgabe passt – sei es schnelle, produktionsreife Transkription (Deepgram), hochpräzise Mehrsprachenfähigkeit (Whisper) oder fortschrittliches Verständnis und Sprachabdeckung (ElevenLabs).
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Nutzungsanleitung
|
||||
|
||||
Transkribiere Audio- und Videodateien zu Text mit führenden KI-Anbietern. Unterstützt mehrere Sprachen, Zeitstempel und Sprechererkennung.
|
||||
|
||||
## Tools
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
Transkribiere Audio zu Text mit OpenAI Whisper
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Ja | STT-Anbieter \(whisper\) |
|
||||
| `apiKey` | string | Ja | OpenAI API-Schlüssel |
|
||||
| `model` | string | Nein | Zu verwendendes Whisper-Modell \(Standard: whisper-1\) |
|
||||
| `audioFile` | file | Nein | Zu transkribierende Audio- oder Videodatei |
|
||||
| `audioFileReference` | file | Nein | Verweis auf Audio-/Videodatei aus vorherigen Blöcken |
|
||||
| `audioUrl` | string | Nein | URL zu Audio- oder Videodatei |
|
||||
| `language` | string | Nein | Sprachcode \(z.B. "en", "es", "fr"\) oder "auto" für automatische Erkennung |
|
||||
| `timestamps` | string | Nein | Zeitstempel-Granularität: none, sentence oder word |
|
||||
| `translateToEnglish` | boolean | Nein | Audio ins Englische übersetzen |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Vollständig transkribierter Text |
|
||||
| `segments` | array | Segmente mit Zeitstempeln |
|
||||
| `language` | string | Erkannte oder angegebene Sprache |
|
||||
| `duration` | number | Audiodauer in Sekunden |
|
||||
| `confidence` | number | Gesamter Konfidenzwert |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
Audio mit Deepgram in Text transkribieren
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Ja | STT-Anbieter (deepgram) |
|
||||
| `apiKey` | string | Ja | Deepgram API-Schlüssel |
|
||||
| `model` | string | Nein | Zu verwendendes Deepgram-Modell (nova-3, nova-2, whisper-large, etc.) |
|
||||
| `audioFile` | file | Nein | Zu transkribierendes Audio- oder Videodatei |
|
||||
| `audioFileReference` | file | Nein | Referenz auf Audio-/Videodatei aus vorherigen Blöcken |
|
||||
| `audioUrl` | string | Nein | URL zu Audio- oder Videodatei |
|
||||
| `language` | string | Nein | Sprachcode (z.B. "en", "es", "fr") oder "auto" für automatische Erkennung |
|
||||
| `timestamps` | string | Nein | Zeitstempel-Granularität: none, sentence oder word |
|
||||
| `diarization` | boolean | Nein | Sprechererkennung aktivieren |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Vollständig transkribierter Text |
|
||||
| `segments` | array | Segmente mit Zeitstempeln und Sprecherkennungen |
|
||||
| `language` | string | Erkannte oder angegebene Sprache |
|
||||
| `duration` | number | Audiodauer in Sekunden |
|
||||
| `confidence` | number | Gesamter Konfidenzwert |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
Audio in Text transkribieren mit ElevenLabs
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Ja | STT-Anbieter \(elevenlabs\) |
|
||||
| `apiKey` | string | Ja | ElevenLabs API-Schlüssel |
|
||||
| `model` | string | Nein | Zu verwendenes ElevenLabs-Modell \(scribe_v1, scribe_v1_experimental\) |
|
||||
| `audioFile` | file | Nein | Audio- oder Videodatei zur Transkription |
|
||||
| `audioFileReference` | file | Nein | Referenz zu Audio-/Videodatei aus vorherigen Blöcken |
|
||||
| `audioUrl` | string | Nein | URL zu Audio- oder Videodatei |
|
||||
| `language` | string | Nein | Sprachcode \(z.B. "en", "es", "fr"\) oder "auto" für automatische Erkennung |
|
||||
| `timestamps` | string | Nein | Zeitstempel-Granularität: none, sentence oder word |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Vollständig transkribierter Text |
|
||||
| `segments` | array | Segmente mit Zeitstempeln |
|
||||
| `language` | string | Erkannte oder angegebene Sprache |
|
||||
| `duration` | number | Audiodauer in Sekunden |
|
||||
| `confidence` | number | Gesamter Konfidenzwert |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
- Typ: `stt`
|
||||
@@ -42,10 +42,10 @@ The user prompt represents the primary input data for inference processing. This
|
||||
|
||||
The Agent block supports multiple LLM providers through a unified inference interface. Available models include:
|
||||
|
||||
- **OpenAI**: GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **OpenAI**: GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 4.5 Sonnet, Claude Opus 4.1
|
||||
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Other Providers**: Groq, Cerebras, xAI, DeepSeek
|
||||
- **Other Providers**: Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
|
||||
- **Local Models**: Ollama-compatible models
|
||||
|
||||
### Temperature
|
||||
|
||||
177
apps/docs/content/docs/en/tools/calendly.mdx
Normal file
177
apps/docs/content/docs/en/tools/calendly.mdx
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Manage Calendly scheduling and events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/) is a popular scheduling automation platform that helps you book meetings, events, and appointments with ease. With Calendly, teams and individuals can streamline scheduling, reduce back-and-forth emails, and automate tasks around events.
|
||||
|
||||
With the Sim Calendly integration, your agents can:
|
||||
|
||||
- **Retrieve information about your account and scheduled events**: Use tools to fetch user info, event types, and scheduled events for analysis or automation.
|
||||
- **Manage event types and scheduling**: Access and list available event types for users or organizations, retrieve details about specific event types, and monitor scheduled meetings and invitee data.
|
||||
- **Automate follow-ups and workflows**: When users schedule, reschedule, or cancel meetings, Sim agents can automatically trigger corresponding workflows—such as sending reminders, updating CRMs, or notifying participants.
|
||||
- **Integrate easily using webhooks**: Set up Sim workflows to respond to real-time Calendly webhook events, including when invitees schedule, cancel, or interact with routing forms.
|
||||
|
||||
Whether you want to automate meeting prep, manage invites, or run custom workflows in response to scheduling activity, the Calendly tools in Sim give you flexible and secure access. Unlock new automation by reacting instantly to scheduling changes—streamlining your team's operations and communications.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Calendly into your workflow. Manage event types, scheduled events, invitees, and webhooks. Can also trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
Get information about the currently authenticated Calendly user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Current user information |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
Retrieve a list of all event types for a user or organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | No | Return only event types that belong to this user \(URI format\) |
|
||||
| `organization` | string | No | Return only event types that belong to this organization \(URI format\) |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "name:asc", "name:desc"\) |
|
||||
| `active` | boolean | No | When true, show only active event types. When false or unchecked, show all event types \(both active and inactive\). |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of event type objects |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
Get detailed information about a specific event type
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventTypeUuid` | string | Yes | Event type UUID \(can be full URI or just the UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Event type details |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
Retrieve a list of scheduled events for a user or organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | No | Return events that belong to this user \(URI format\). Either "user" or "organization" must be provided. |
|
||||
| `organization` | string | No | Return events that belong to this organization \(URI format\). Either "user" or "organization" must be provided. |
|
||||
| `invitee_email` | string | No | Return events where invitee has this email |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `max_start_time` | string | No | Return events with start time before this time \(ISO 8601 format\) |
|
||||
| `min_start_time` | string | No | Return events with start time after this time \(ISO 8601 format\) |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | No | Filter by status \("active" or "canceled"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of scheduled event objects |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
Get detailed information about a specific scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID \(can be full URI or just the UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Scheduled event details |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
Retrieve a list of invitees for a scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID \(can be full URI or just the UUID\) |
|
||||
| `count` | number | No | Number of results per page \(default: 20, max: 100\) |
|
||||
| `email` | string | No | Filter invitees by email address |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
| `sort` | string | No | Sort order for results \(e.g., "created_at:asc", "created_at:desc"\) |
|
||||
| `status` | string | No | Filter by status \("active" or "canceled"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of invitee objects |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
Cancel a scheduled event
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID to cancel \(can be full URI or just the UUID\) |
|
||||
| `reason` | string | No | Reason for cancellation \(will be sent to invitees\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Cancellation details |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `calendly`
|
||||
@@ -42,8 +42,8 @@ Fetch and filter issues from Linear
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `teamId` | string | Yes | Linear team ID |
|
||||
| `projectId` | string | Yes | Linear project ID |
|
||||
| `teamId` | string | No | Linear team ID to filter by |
|
||||
| `projectId` | string | No | Linear project ID to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -76,7 +76,7 @@ Create a new issue in Linear
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `teamId` | string | Yes | Linear team ID |
|
||||
| `projectId` | string | Yes | Linear project ID |
|
||||
| `projectId` | string | No | Linear project ID |
|
||||
| `title` | string | Yes | Issue title |
|
||||
| `description` | string | No | Issue description |
|
||||
|
||||
@@ -240,7 +240,7 @@ Edit a comment in Linear
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `commentId` | string | Yes | Comment ID to update |
|
||||
| `body` | string | Yes | New comment text \(supports Markdown\) |
|
||||
| `body` | string | No | New comment text \(supports Markdown\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -640,7 +640,7 @@ Add an attachment to an issue in Linear
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `issueId` | string | Yes | Issue ID to attach to |
|
||||
| `url` | string | Yes | URL of the attachment |
|
||||
| `title` | string | No | Attachment title |
|
||||
| `title` | string | Yes | Attachment title |
|
||||
| `subtitle` | string | No | Attachment subtitle/description |
|
||||
|
||||
#### Output
|
||||
@@ -676,7 +676,7 @@ Update an attachment metadata in Linear
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `attachmentId` | string | Yes | Attachment ID to update |
|
||||
| `title` | string | No | New attachment title |
|
||||
| `title` | string | Yes | New attachment title |
|
||||
| `subtitle` | string | No | New attachment subtitle |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -26,9 +26,11 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory. If a memory with this ID already exists, the new data will be appended to it. |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If a memory with this conversationId already exists for this block, the new message will be appended to it. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `role` | string | Yes | Role for agent memory \(user, assistant, or system\) |
|
||||
| `content` | string | Yes | Content for agent memory |
|
||||
| `blockId` | string | No | Optional block ID. If not provided, uses the current block ID from execution context, or defaults to "default". |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -40,20 +42,23 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Retrieve a specific memory by its ID
|
||||
Retrieve memory by conversationId, blockId, blockName, or a combination. Returns all matching memories.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory to retrieve |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, returns all memories for this conversation across all blocks. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, returns all memories for this block across all conversations. If provided with conversationId, returns memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, returns all memories for blocks with this name. If provided with conversationId, returns memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the memory was retrieved successfully |
|
||||
| `memories` | array | Array of memory data for the requested ID |
|
||||
| `memories` | array | Array of memory objects with conversationId, blockId, blockName, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
@@ -71,19 +76,22 @@ Retrieve all memories from the database
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all memories were retrieved successfully |
|
||||
| `memories` | array | Array of all memory objects with keys, types, and data |
|
||||
| `memories` | array | Array of all memory objects with key, conversationId, blockId, blockName, and data fields |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Delete a specific memory by its ID
|
||||
Delete memories by conversationId, blockId, blockName, or a combination. Supports bulk deletion.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | Yes | Identifier for the memory to delete |
|
||||
| `conversationId` | string | No | Conversation identifier \(e.g., user-123, session-abc\). If provided alone, deletes all memories for this conversation across all blocks. |
|
||||
| `id` | string | No | Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility. |
|
||||
| `blockId` | string | No | Block identifier. If provided alone, deletes all memories for this block across all conversations. If provided with conversationId, deletes memories for that specific conversation in this block. |
|
||||
| `blockName` | string | No | Block name. Alternative to blockId. If provided alone, deletes all memories for blocks with this name. If provided with conversationId, deletes memories for that conversation in blocks with this name. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"arxiv",
|
||||
"asana",
|
||||
"browser_use",
|
||||
"calendly",
|
||||
"clay",
|
||||
"confluence",
|
||||
"discord",
|
||||
@@ -39,6 +40,7 @@
|
||||
"mistral_parse",
|
||||
"mongodb",
|
||||
"mysql",
|
||||
"neo4j",
|
||||
"notion",
|
||||
"onedrive",
|
||||
"openai",
|
||||
@@ -59,6 +61,7 @@
|
||||
"stagehand",
|
||||
"stagehand_agent",
|
||||
"stripe",
|
||||
"stt",
|
||||
"supabase",
|
||||
"tavily",
|
||||
"telegram",
|
||||
|
||||
@@ -27,7 +27,7 @@ In Sim, the Microsoft Excel integration provides seamless access to spreadsheet
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table.
|
||||
Integrate Microsoft Excel into the workflow. Can read, write, update, add to table, and create new worksheets.
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,23 @@ Add new rows to a Microsoft Excel table
|
||||
| `values` | array | Array of rows that were added to the table |
|
||||
| `metadata` | object | Spreadsheet metadata |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
Create a new worksheet (sheet) in a Microsoft Excel workbook
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the Excel workbook to add the worksheet to |
|
||||
| `worksheetName` | string | Yes | The name of the new worksheet. Must be unique within the workbook and cannot exceed 31 characters |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | object | Details of the newly created worksheet |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
176
apps/docs/content/docs/en/tools/neo4j.mdx
Normal file
176
apps/docs/content/docs/en/tools/neo4j.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Connect to Neo4j graph database
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Neo4j graph database into the workflow. Can query, create, merge, update, and delete nodes and relationships.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
Execute MATCH queries to read nodes and relationships from Neo4j graph database. For best performance and to prevent large result sets, include LIMIT in your query (e.g.,
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query to execute \(typically MATCH statements\) |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object. Use for any dynamic values including LIMIT \(e.g., query: "MATCH \(n\) RETURN n LIMIT $limit", parameters: \{limit: 100\}\). |
|
||||
| `parameters` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `records` | array | Array of records returned from the query |
|
||||
| `recordCount` | number | Number of records returned |
|
||||
| `summary` | json | Query execution summary with timing and counters |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
Execute CREATE statements to add new nodes and relationships to Neo4j graph database
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher CREATE statement to execute |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Creation summary with counters for nodes and relationships created |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
Execute MERGE statements to find or create nodes and relationships in Neo4j (upsert operation)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher MERGE statement to execute |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Merge summary with counters for nodes/relationships created or matched |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Execute SET statements to update properties of existing nodes and relationships in Neo4j
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query with MATCH and SET statements to update properties |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Update summary with counters for properties set |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Execute DELETE or DETACH DELETE statements to remove nodes and relationships from Neo4j
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query with MATCH and DELETE/DETACH DELETE statements |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
| `detach` | boolean | No | Whether to use DETACH DELETE to remove relationships before deleting nodes |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `summary` | json | Delete summary with counters for nodes and relationships deleted |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
Execute arbitrary Cypher queries on Neo4j graph database for complex operations
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Yes | Neo4j server hostname or IP address |
|
||||
| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) |
|
||||
| `database` | string | Yes | Database name to connect to |
|
||||
| `username` | string | Yes | Neo4j username |
|
||||
| `password` | string | Yes | Neo4j password |
|
||||
| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Yes | Cypher query to execute \(any valid Cypher statement\) |
|
||||
| `parameters` | object | No | Parameters for the Cypher query as a JSON object |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `records` | array | Array of records returned from the query |
|
||||
| `recordCount` | number | Number of records returned |
|
||||
| `summary` | json | Execution summary with timing and counters |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `neo4j`
|
||||
122
apps/docs/content/docs/en/tools/stt.mdx
Normal file
122
apps/docs/content/docs/en/tools/stt.mdx
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Speech-to-Text
|
||||
description: Convert speech to text using AI
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Transcribe speech to text using state-of-the-art AI models from leading providers. The Sim Speech-to-Text (STT) tools allow you to convert audio and video files into accurate transcripts, supporting multiple languages, timestamps, and optional translation.
|
||||
|
||||
Supported providers:
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)**: Advanced open-source STT model from OpenAI. Supports models such as `whisper-1` and handles a wide variety of languages and audio formats.
|
||||
- **[Deepgram](https://deepgram.com/)**: Real-time and batch STT API with deep learning models like `nova-3`, `nova-2`, and `whisper-large`. Offers features like diarization, intent recognition, and industry-specific tuning.
|
||||
- **[ElevenLabs](https://elevenlabs.io/)**: Known for high-quality speech AI, ElevenLabs provides STT models focused on accuracy and natural language understanding for numerous languages and dialects.
|
||||
|
||||
Choose the provider and model best suited to your task—whether fast, production-grade transcription (Deepgram), highly accurate multi-language capability (Whisper), or advanced understanding and language coverage (ElevenLabs).
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Transcribe audio and video files to text using leading AI providers. Supports multiple languages, timestamps, and speaker diarization.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
Transcribe audio to text using OpenAI Whisper
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Yes | STT provider \(whisper\) |
|
||||
| `apiKey` | string | Yes | OpenAI API key |
|
||||
| `model` | string | No | Whisper model to use \(default: whisper-1\) |
|
||||
| `audioFile` | file | No | Audio or video file to transcribe |
|
||||
| `audioFileReference` | file | No | Reference to audio/video file from previous blocks |
|
||||
| `audioUrl` | string | No | URL to audio or video file |
|
||||
| `language` | string | No | Language code \(e.g., "en", "es", "fr"\) or "auto" for auto-detection |
|
||||
| `timestamps` | string | No | Timestamp granularity: none, sentence, or word |
|
||||
| `translateToEnglish` | boolean | No | Translate audio to English |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Full transcribed text |
|
||||
| `segments` | array | Timestamped segments |
|
||||
| `language` | string | Detected or specified language |
|
||||
| `duration` | number | Audio duration in seconds |
|
||||
| `confidence` | number | Overall confidence score |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
Transcribe audio to text using Deepgram
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Yes | STT provider \(deepgram\) |
|
||||
| `apiKey` | string | Yes | Deepgram API key |
|
||||
| `model` | string | No | Deepgram model to use \(nova-3, nova-2, whisper-large, etc.\) |
|
||||
| `audioFile` | file | No | Audio or video file to transcribe |
|
||||
| `audioFileReference` | file | No | Reference to audio/video file from previous blocks |
|
||||
| `audioUrl` | string | No | URL to audio or video file |
|
||||
| `language` | string | No | Language code \(e.g., "en", "es", "fr"\) or "auto" for auto-detection |
|
||||
| `timestamps` | string | No | Timestamp granularity: none, sentence, or word |
|
||||
| `diarization` | boolean | No | Enable speaker diarization |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Full transcribed text |
|
||||
| `segments` | array | Timestamped segments with speaker labels |
|
||||
| `language` | string | Detected or specified language |
|
||||
| `duration` | number | Audio duration in seconds |
|
||||
| `confidence` | number | Overall confidence score |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
Transcribe audio to text using ElevenLabs
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Yes | STT provider \(elevenlabs\) |
|
||||
| `apiKey` | string | Yes | ElevenLabs API key |
|
||||
| `model` | string | No | ElevenLabs model to use \(scribe_v1, scribe_v1_experimental\) |
|
||||
| `audioFile` | file | No | Audio or video file to transcribe |
|
||||
| `audioFileReference` | file | No | Reference to audio/video file from previous blocks |
|
||||
| `audioUrl` | string | No | URL to audio or video file |
|
||||
| `language` | string | No | Language code \(e.g., "en", "es", "fr"\) or "auto" for auto-detection |
|
||||
| `timestamps` | string | No | Timestamp granularity: none, sentence, or word |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Full transcribed text |
|
||||
| `segments` | array | Timestamped segments |
|
||||
| `language` | string | Detected or specified language |
|
||||
| `duration` | number | Audio duration in seconds |
|
||||
| `confidence` | number | Overall confidence score |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `stt`
|
||||
@@ -42,11 +42,11 @@ El prompt del usuario representa los datos de entrada principales para el proces
|
||||
|
||||
El bloque Agente admite múltiples proveedores de LLM a través de una interfaz de inferencia unificada. Los modelos disponibles incluyen:
|
||||
|
||||
- **OpenAI**: GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **OpenAI**: GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 4.5 Sonnet, Claude Opus 4.1
|
||||
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Otros proveedores**: Groq, Cerebras, xAI, DeepSeek
|
||||
- **Modelos locales**: Modelos compatibles con Ollama
|
||||
- **Otros proveedores**: Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
|
||||
- **Modelos locales**: modelos compatibles con Ollama
|
||||
|
||||
### Temperatura
|
||||
|
||||
|
||||
172
apps/docs/content/docs/es/tools/calendly.mdx
Normal file
172
apps/docs/content/docs/es/tools/calendly.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Gestiona programación y eventos de Calendly
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/) es una popular plataforma de automatización de programación que te ayuda a reservar reuniones, eventos y citas con facilidad. Con Calendly, los equipos e individuos pueden simplificar la programación, reducir los intercambios de correos electrónicos y automatizar tareas relacionadas con eventos.
|
||||
|
||||
Con la integración de Sim Calendly, tus agentes pueden:
|
||||
|
||||
- **Recuperar información sobre tu cuenta y eventos programados**: Utiliza herramientas para obtener información de usuario, tipos de eventos y eventos programados para análisis o automatización.
|
||||
- **Gestionar tipos de eventos y programación**: Accede y lista los tipos de eventos disponibles para usuarios u organizaciones, recupera detalles sobre tipos de eventos específicos y monitorea reuniones programadas y datos de invitados.
|
||||
- **Automatizar seguimientos y flujos de trabajo**: Cuando los usuarios programan, reprograman o cancelan reuniones, los agentes de Sim pueden activar automáticamente los flujos de trabajo correspondientes, como enviar recordatorios, actualizar CRMs o notificar a los participantes.
|
||||
- **Integración fácil mediante webhooks**: Configura flujos de trabajo de Sim para responder a eventos de webhook de Calendly en tiempo real, incluyendo cuando los invitados programan, cancelan o interactúan con formularios de enrutamiento.
|
||||
|
||||
Ya sea que quieras automatizar la preparación de reuniones, gestionar invitaciones o ejecutar flujos de trabajo personalizados en respuesta a la actividad de programación, las herramientas de Calendly en Sim te brindan acceso flexible y seguro. Desbloquea nuevas automatizaciones reaccionando instantáneamente a los cambios de programación, simplificando las operaciones y comunicaciones de tu equipo.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra Calendly en tu flujo de trabajo. Gestiona tipos de eventos, eventos programados, invitados y webhooks. También puede activar flujos de trabajo basados en eventos de webhook de Calendly (invitado programado, invitado cancelado, formulario de enrutamiento enviado). Requiere un token de acceso personal.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
Obtener información sobre el usuario de Calendly actualmente autenticado
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Información del usuario actual |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
Recuperar una lista de todos los tipos de eventos para un usuario u organización
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `user` | string | No | Devolver solo tipos de eventos que pertenecen a este usuario \(formato URI\) |
|
||||
| `organization` | string | No | Devolver solo tipos de eventos que pertenecen a esta organización \(formato URI\) |
|
||||
| `count` | number | No | Número de resultados por página \(predeterminado: 20, máximo: 100\) |
|
||||
| `pageToken` | string | No | Token de página para paginación |
|
||||
| `sort` | string | No | Orden de clasificación para resultados \(p. ej., "name:asc", "name:desc"\) |
|
||||
| `active` | boolean | No | Cuando es verdadero, muestra solo tipos de eventos activos. Cuando es falso o no está marcado, muestra todos los tipos de eventos \(tanto activos como inactivos\). |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array de objetos de tipo de evento |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
Obtener información detallada sobre un tipo de evento específico
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `eventTypeUuid` | string | Sí | UUID del tipo de evento \(puede ser URI completa o solo el UUID\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Detalles del tipo de evento |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
Recuperar una lista de eventos programados para un usuario u organización
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `user` | string | No | Devolver eventos que pertenecen a este usuario \(formato URI\). Se debe proporcionar "usuario" u "organización". |
|
||||
| `organization` | string | No | Devolver eventos que pertenecen a esta organización \(formato URI\). Se debe proporcionar "usuario" u "organización". |
|
||||
| `invitee_email` | string | No | Devolver eventos donde el invitado tiene este correo electrónico |
|
||||
| `count` | number | No | Número de resultados por página \(predeterminado: 20, máximo: 100\) |
|
||||
| `max_start_time` | string | No | Devolver eventos con hora de inicio antes de esta hora \(formato ISO 8601\) |
|
||||
| `min_start_time` | string | No | Devolver eventos con hora de inicio después de esta hora \(formato ISO 8601\) |
|
||||
| `pageToken` | string | No | Token de página para paginación |
|
||||
| `sort` | string | No | Orden de clasificación para resultados \(p. ej., "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | No | Filtrar por estado \("active" o "canceled"\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array de objetos de eventos programados |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
Obtener información detallada sobre un evento programado específico
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `eventUuid` | string | Sí | UUID del evento programado \(puede ser URI completa o solo el UUID\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Detalles del evento programado |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
Recuperar una lista de invitados para un evento programado
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `eventUuid` | string | Sí | UUID del evento programado \(puede ser URI completa o solo el UUID\) |
|
||||
| `count` | number | No | Número de resultados por página \(predeterminado: 20, máximo: 100\) |
|
||||
| `email` | string | No | Filtrar invitados por dirección de correo electrónico |
|
||||
| `pageToken` | string | No | Token de página para paginación |
|
||||
| `sort` | string | No | Orden de clasificación para resultados \(p. ej., "created_at:asc", "created_at:desc"\) |
|
||||
| `status` | string | No | Filtrar por estado \("active" o "canceled"\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array de objetos de invitados |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
Cancelar un evento programado
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Sí | Token de acceso personal de Calendly |
|
||||
| `eventUuid` | string | Sí | UUID del evento programado a cancelar \(puede ser URI completa o solo el UUID\) |
|
||||
| `reason` | string | No | Motivo de la cancelación \(se enviará a los invitados\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Detalles de la cancelación |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
- Tipo: `calendly`
|
||||
@@ -24,9 +24,11 @@ Añade una nueva memoria a la base de datos o agrega a una memoria existente con
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `id` | string | Sí | Identificador para la memoria. Si ya existe una memoria con este ID, los nuevos datos se añadirán a ella. |
|
||||
| `role` | string | Sí | Rol para la memoria del agente \(usuario, asistente o sistema\) |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si ya existe una memoria con este conversationId para este bloque, el nuevo mensaje se añadirá a ella. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `role` | string | Sí | Rol para la memoria del agente (user, assistant o system) |
|
||||
| `content` | string | Sí | Contenido para la memoria del agente |
|
||||
| `blockId` | string | No | ID de bloque opcional. Si no se proporciona, utiliza el ID del bloque actual del contexto de ejecución, o por defecto "default". |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -38,20 +40,23 @@ Añade una nueva memoria a la base de datos o agrega a una memoria existente con
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Recuperar una memoria específica por su ID
|
||||
Recuperar memoria por conversationId, blockId, blockName o una combinación. Devuelve todas las memorias coincidentes.
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `id` | string | Sí | Identificador de la memoria a recuperar |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si se proporciona solo, devuelve todas las memorias para esta conversación en todos los bloques. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `blockId` | string | No | Identificador de bloque. Si se proporciona solo, devuelve todas las memorias para este bloque en todas las conversaciones. Si se proporciona con conversationId, devuelve las memorias para esa conversación específica en este bloque. |
|
||||
| `blockName` | string | No | Nombre del bloque. Alternativa a blockId. Si se proporciona solo, devuelve todas las memorias para bloques con este nombre. Si se proporciona con conversationId, devuelve las memorias para esa conversación en bloques con este nombre. |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indica si la memoria se recuperó correctamente |
|
||||
| `memories` | array | Array de datos de memoria para el ID solicitado |
|
||||
| `success` | boolean | Si la memoria fue recuperada con éxito |
|
||||
| `memories` | array | Array de objetos de memoria con campos conversationId, blockId, blockName y data |
|
||||
| `message` | string | Mensaje de éxito o error |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
@@ -68,20 +73,23 @@ Recuperar todas las memorias de la base de datos
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indica si todas las memorias se recuperaron correctamente |
|
||||
| `memories` | array | Array de todos los objetos de memoria con claves, tipos y datos |
|
||||
| `success` | boolean | Si todas las memorias fueron recuperadas con éxito |
|
||||
| `memories` | array | Array de todos los objetos de memoria con campos key, conversationId, blockId, blockName y data |
|
||||
| `message` | string | Mensaje de éxito o error |
|
||||
| `error` | string | Mensaje de error si la operación falló |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Eliminar una memoria específica por su ID
|
||||
Eliminar memorias por conversationId, blockId, blockName o una combinación. Admite eliminación masiva.
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `id` | string | Sí | Identificador de la memoria a eliminar |
|
||||
| `conversationId` | string | No | Identificador de conversación (p. ej., user-123, session-abc). Si se proporciona solo, elimina todas las memorias para esta conversación en todos los bloques. |
|
||||
| `id` | string | No | Parámetro heredado para el identificador de conversación. Use conversationId en su lugar. Proporcionado para compatibilidad con versiones anteriores. |
|
||||
| `blockId` | string | No | Identificador de bloque. Si se proporciona solo, elimina todas las memorias para este bloque en todas las conversaciones. Si se proporciona con conversationId, elimina las memorias para esa conversación específica en este bloque. |
|
||||
| `blockName` | string | No | Nombre del bloque. Alternativa a blockId. Si se proporciona solo, elimina todas las memorias para bloques con este nombre. Si se proporciona con conversationId, elimina las memorias para esa conversación en bloques con este nombre. |
|
||||
|
||||
#### Salida
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ En Sim, la integración de Microsoft Excel proporciona acceso fluido a la funcio
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra Microsoft Excel en el flujo de trabajo. Puede leer, escribir, actualizar y añadir a la tabla. Requiere OAuth.
|
||||
Integra Microsoft Excel en el flujo de trabajo. Puede leer, escribir, actualizar, añadir a tablas y crear nuevas hojas de cálculo.
|
||||
|
||||
## Herramientas
|
||||
|
||||
@@ -91,6 +91,23 @@ Añadir nuevas filas a una tabla de Microsoft Excel
|
||||
| `values` | array | Matriz de filas que fueron añadidas a la tabla |
|
||||
| `metadata` | object | Metadatos de la hoja de cálculo |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
Crear una nueva hoja de cálculo en un libro de Microsoft Excel
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Sí | El ID del libro de Excel al que añadir la hoja de cálculo |
|
||||
| `worksheetName` | string | Sí | El nombre de la nueva hoja de cálculo. Debe ser único dentro del libro y no puede exceder los 31 caracteres |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | object | Detalles de la hoja de cálculo recién creada |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
|
||||
172
apps/docs/content/docs/es/tools/neo4j.mdx
Normal file
172
apps/docs/content/docs/es/tools/neo4j.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Conectar a la base de datos de grafos Neo4j
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra la base de datos de grafos Neo4j en el flujo de trabajo. Puede consultar, crear, fusionar, actualizar y eliminar nodos y relaciones.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
Ejecuta consultas MATCH para leer nodos y relaciones de la base de datos de grafos Neo4j. Para un mejor rendimiento y para evitar grandes conjuntos de resultados, incluye LIMIT en tu consulta (por ejemplo,
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para el protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Consulta Cypher para ejecutar \(normalmente declaraciones MATCH\) |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como un objeto JSON. Úsalo para cualquier valor dinámico incluyendo LIMIT \(por ejemplo, query: "MATCH \(n\) RETURN n LIMIT $limit", parameters: \{limit: 100\}\). |
|
||||
| `parameters` | string | No | Sin descripción |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `records` | array | Array de registros devueltos por la consulta |
|
||||
| `recordCount` | number | Número de registros devueltos |
|
||||
| `summary` | json | Resumen de ejecución de la consulta con tiempos y contadores |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
Ejecuta sentencias CREATE para añadir nuevos nodos y relaciones a la base de datos de grafos Neo4j
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para el protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Sentencia Cypher CREATE a ejecutar |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como objeto JSON |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `summary` | json | Resumen de creación con contadores de nodos y relaciones creados |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
Ejecuta sentencias MERGE para encontrar o crear nodos y relaciones en Neo4j (operación upsert)
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para el protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Sentencia Cypher MERGE a ejecutar |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como objeto JSON |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `summary` | json | Resumen de fusión con contadores para nodos/relaciones creados o coincidentes |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Ejecuta declaraciones SET para actualizar propiedades de nodos y relaciones existentes en Neo4j
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Consulta Cypher con declaraciones MATCH y SET para actualizar propiedades |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como objeto JSON |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `summary` | json | Resumen de actualización con contadores para propiedades establecidas |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Ejecuta declaraciones DELETE o DETACH DELETE para eliminar nodos y relaciones de Neo4j
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Consulta Cypher con declaraciones MATCH y DELETE/DETACH DELETE |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como objeto JSON |
|
||||
| `detach` | boolean | No | Si se debe usar DETACH DELETE para eliminar relaciones antes de eliminar nodos |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `summary` | json | Resumen de eliminación con contadores para nodos y relaciones eliminados |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
Ejecuta consultas Cypher arbitrarias en la base de datos de grafos Neo4j para operaciones complejas
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Sí | Nombre de host o dirección IP del servidor Neo4j |
|
||||
| `port` | number | Sí | Puerto del servidor Neo4j \(predeterminado: 7687 para el protocolo Bolt\) |
|
||||
| `database` | string | Sí | Nombre de la base de datos a la que conectarse |
|
||||
| `username` | string | Sí | Nombre de usuario de Neo4j |
|
||||
| `password` | string | Sí | Contraseña de Neo4j |
|
||||
| `encryption` | string | No | Modo de cifrado de conexión \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Sí | Consulta Cypher a ejecutar \(cualquier declaración Cypher válida\) |
|
||||
| `parameters` | object | No | Parámetros para la consulta Cypher como un objeto JSON |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Mensaje de estado de la operación |
|
||||
| `records` | array | Array de registros devueltos por la consulta |
|
||||
| `recordCount` | number | Número de registros devueltos |
|
||||
| `summary` | json | Resumen de ejecución con tiempos y contadores |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
- Tipo: `neo4j`
|
||||
117
apps/docs/content/docs/es/tools/stt.mdx
Normal file
117
apps/docs/content/docs/es/tools/stt.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Voz a texto
|
||||
description: Convierte voz a texto usando IA
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Transcribe voz a texto utilizando modelos de IA de vanguardia de proveedores líderes. Las herramientas de Sim de voz a texto (STT) te permiten convertir archivos de audio y video en transcripciones precisas, con soporte para múltiples idiomas, marcas de tiempo y traducción opcional.
|
||||
|
||||
Proveedores compatibles:
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)**: Modelo STT avanzado de código abierto de OpenAI. Compatible con modelos como `whisper-1` y maneja una amplia variedad de idiomas y formatos de audio.
|
||||
- **[Deepgram](https://deepgram.com/)**: API de STT en tiempo real y por lotes con modelos de aprendizaje profundo como `nova-3`, `nova-2` y `whisper-large`. Ofrece características como diarización, reconocimiento de intención y ajuste específico para industrias.
|
||||
- **[ElevenLabs](https://elevenlabs.io/)**: Conocido por su IA de voz de alta calidad, ElevenLabs proporciona modelos STT enfocados en precisión y comprensión del lenguaje natural para numerosos idiomas y dialectos.
|
||||
|
||||
Elige el proveedor y modelo que mejor se adapte a tu tarea, ya sea transcripción rápida de nivel de producción (Deepgram), capacidad multilingüe altamente precisa (Whisper) o comprensión avanzada y cobertura de idiomas (ElevenLabs).
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Transcribe archivos de audio y video a texto utilizando proveedores de IA líderes. Compatible con múltiples idiomas, marcas de tiempo y diarización de hablantes.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
Transcribe audio a texto usando OpenAI Whisper
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Sí | Proveedor STT \(whisper\) |
|
||||
| `apiKey` | string | Sí | Clave API de OpenAI |
|
||||
| `model` | string | No | Modelo Whisper a utilizar \(predeterminado: whisper-1\) |
|
||||
| `audioFile` | file | No | Archivo de audio o video para transcribir |
|
||||
| `audioFileReference` | file | No | Referencia a archivo de audio/video de bloques anteriores |
|
||||
| `audioUrl` | string | No | URL al archivo de audio o video |
|
||||
| `language` | string | No | Código de idioma \(p.ej., "en", "es", "fr"\) o "auto" para detección automática |
|
||||
| `timestamps` | string | No | Granularidad de marcas de tiempo: none, sentence, o word |
|
||||
| `translateToEnglish` | boolean | No | Traducir audio a inglés |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Texto transcrito completo |
|
||||
| `segments` | array | Segmentos con marcas de tiempo |
|
||||
| `language` | string | Idioma detectado o especificado |
|
||||
| `duration` | number | Duración del audio en segundos |
|
||||
| `confidence` | number | Puntuación de confianza general |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
Transcribe audio a texto usando Deepgram
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Sí | Proveedor STT \(deepgram\) |
|
||||
| `apiKey` | string | Sí | Clave API de Deepgram |
|
||||
| `model` | string | No | Modelo de Deepgram a utilizar \(nova-3, nova-2, whisper-large, etc.\) |
|
||||
| `audioFile` | file | No | Archivo de audio o video para transcribir |
|
||||
| `audioFileReference` | file | No | Referencia al archivo de audio/video de bloques anteriores |
|
||||
| `audioUrl` | string | No | URL al archivo de audio o video |
|
||||
| `language` | string | No | Código de idioma \(p.ej., "en", "es", "fr"\) o "auto" para detección automática |
|
||||
| `timestamps` | string | No | Granularidad de marcas de tiempo: none, sentence, o word |
|
||||
| `diarization` | boolean | No | Habilitar diarización de hablantes |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Texto transcrito completo |
|
||||
| `segments` | array | Segmentos con marcas de tiempo y etiquetas de hablantes |
|
||||
| `language` | string | Idioma detectado o especificado |
|
||||
| `duration` | number | Duración del audio en segundos |
|
||||
| `confidence` | number | Puntuación de confianza general |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
Transcribe audio a texto usando ElevenLabs
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Sí | Proveedor STT \(elevenlabs\) |
|
||||
| `apiKey` | string | Sí | Clave API de ElevenLabs |
|
||||
| `model` | string | No | Modelo de ElevenLabs a utilizar \(scribe_v1, scribe_v1_experimental\) |
|
||||
| `audioFile` | file | No | Archivo de audio o video para transcribir |
|
||||
| `audioFileReference` | file | No | Referencia al archivo de audio/video de bloques anteriores |
|
||||
| `audioUrl` | string | No | URL al archivo de audio o video |
|
||||
| `language` | string | No | Código de idioma \(p.ej., "en", "es", "fr"\) o "auto" para detección automática |
|
||||
| `timestamps` | string | No | Granularidad de marca de tiempo: none, sentence, o word |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Texto transcrito completo |
|
||||
| `segments` | array | Segmentos con marcas de tiempo |
|
||||
| `language` | string | Idioma detectado o especificado |
|
||||
| `duration` | number | Duración del audio en segundos |
|
||||
| `confidence` | number | Puntuación de confianza general |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
- Tipo: `stt`
|
||||
@@ -42,10 +42,10 @@ Le prompt utilisateur représente les données d'entrée principales pour le tra
|
||||
|
||||
Le bloc Agent prend en charge plusieurs fournisseurs de LLM via une interface d'inférence unifiée. Les modèles disponibles comprennent :
|
||||
|
||||
- **OpenAI** : GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic** : Claude 3.7 Sonnet
|
||||
- **OpenAI** : GPT-5.1, GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic** : Claude 4.5 Sonnet, Claude Opus 4.1
|
||||
- **Google** : Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Autres fournisseurs** : Groq, Cerebras, xAI, DeepSeek
|
||||
- **Autres fournisseurs** : Groq, Cerebras, xAI, Azure OpenAI, OpenRouter
|
||||
- **Modèles locaux** : modèles compatibles avec Ollama
|
||||
|
||||
### Température
|
||||
|
||||
172
apps/docs/content/docs/fr/tools/calendly.mdx
Normal file
172
apps/docs/content/docs/fr/tools/calendly.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Gérer la planification et les événements Calendly
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/) est une plateforme populaire d'automatisation de planification qui vous aide à organiser des réunions, des événements et des rendez-vous facilement. Avec Calendly, les équipes et les individus peuvent simplifier la planification, réduire les échanges d'e-mails et automatiser les tâches liées aux événements.
|
||||
|
||||
Avec l'intégration Sim Calendly, vos agents peuvent :
|
||||
|
||||
- **Récupérer des informations sur votre compte et les événements programmés** : Utilisez des outils pour obtenir des informations utilisateur, des types d'événements et des événements programmés pour analyse ou automatisation.
|
||||
- **Gérer les types d'événements et la planification** : Accédez et listez les types d'événements disponibles pour les utilisateurs ou les organisations, récupérez des détails sur des types d'événements spécifiques, et surveillez les réunions programmées et les données des invités.
|
||||
- **Automatiser les suivis et les flux de travail** : Lorsque les utilisateurs planifient, replanifient ou annulent des réunions, les agents Sim peuvent automatiquement déclencher les flux de travail correspondants—comme l'envoi de rappels, la mise à jour des CRM ou la notification des participants.
|
||||
- **S'intégrer facilement à l'aide de webhooks** : Configurez des flux de travail Sim pour répondre aux événements webhook de Calendly en temps réel, notamment lorsque les invités planifient, annulent ou interagissent avec des formulaires de routage.
|
||||
|
||||
Que vous souhaitiez automatiser la préparation des réunions, gérer les invitations ou exécuter des flux de travail personnalisés en réponse à l'activité de planification, les outils Calendly dans Sim vous offrent un accès flexible et sécurisé. Débloquez de nouvelles automatisations en réagissant instantanément aux changements de planification—simplifiant les opérations et les communications de votre équipe.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez Calendly dans votre flux de travail. Gérez les types d'événements, les événements programmés, les invités et les webhooks. Peut également déclencher des flux de travail basés sur les événements webhook de Calendly (invité programmé, invité annulé, formulaire de routage soumis). Nécessite un jeton d'accès personnel.
|
||||
|
||||
## Outils
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
Obtenir des informations sur l'utilisateur Calendly actuellement authentifié
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Informations sur l'utilisateur actuel |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
Récupérer une liste de tous les types d'événements pour un utilisateur ou une organisation
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `user` | string | Non | Renvoie uniquement les types d'événements appartenant à cet utilisateur \(format URI\) |
|
||||
| `organization` | string | Non | Renvoie uniquement les types d'événements appartenant à cette organisation \(format URI\) |
|
||||
| `count` | number | Non | Nombre de résultats par page \(par défaut : 20, max : 100\) |
|
||||
| `pageToken` | string | Non | Jeton de page pour la pagination |
|
||||
| `sort` | string | Non | Ordre de tri pour les résultats \(par ex., "name:asc", "name:desc"\) |
|
||||
| `active` | boolean | Non | Lorsque true, affiche uniquement les types d'événements actifs. Lorsque false ou non coché, affiche tous les types d'événements \(actifs et inactifs\). |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Tableau d'objets de type d'événement |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
Obtenir des informations détaillées sur un type d'événement spécifique
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `eventTypeUuid` | string | Oui | UUID du type d'événement \(peut être l'URI complète ou simplement l'UUID\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Détails du type d'événement |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
Récupérer une liste des événements programmés pour un utilisateur ou une organisation
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `user` | string | Non | Renvoyer les événements appartenant à cet utilisateur \(format URI\). Soit "user" soit "organization" doit être fourni. |
|
||||
| `organization` | string | Non | Renvoyer les événements appartenant à cette organisation \(format URI\). Soit "user" soit "organization" doit être fourni. |
|
||||
| `invitee_email` | string | Non | Renvoyer les événements où l'invité a cette adresse e-mail |
|
||||
| `count` | number | Non | Nombre de résultats par page \(par défaut : 20, max : 100\) |
|
||||
| `max_start_time` | string | Non | Renvoyer les événements avec une heure de début avant cette heure \(format ISO 8601\) |
|
||||
| `min_start_time` | string | Non | Renvoyer les événements avec une heure de début après cette heure \(format ISO 8601\) |
|
||||
| `pageToken` | string | Non | Jeton de page pour la pagination |
|
||||
| `sort` | string | Non | Ordre de tri pour les résultats \(par ex., "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | Non | Filtrer par statut \("active" ou "canceled"\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Tableau d'objets d'événements programmés |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
Obtenir des informations détaillées sur un événement programmé spécifique
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `eventUuid` | string | Oui | UUID de l'événement programmé \(peut être l'URI complète ou simplement l'UUID\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Détails de l'événement programmé |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
Récupérer une liste des invités pour un événement programmé
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `eventUuid` | string | Oui | UUID de l'événement programmé \(peut être l'URI complète ou simplement l'UUID\) |
|
||||
| `count` | number | Non | Nombre de résultats par page \(par défaut : 20, max : 100\) |
|
||||
| `email` | string | Non | Filtrer les invités par adresse email |
|
||||
| `pageToken` | string | Non | Jeton de page pour la pagination |
|
||||
| `sort` | string | Non | Ordre de tri pour les résultats \(par ex., "created_at:asc", "created_at:desc"\) |
|
||||
| `status` | string | Non | Filtrer par statut \("active" ou "canceled"\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Tableau d'objets invités |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
Annuler un événement programmé
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Oui | Jeton d'accès personnel Calendly |
|
||||
| `eventUuid` | string | Oui | UUID de l'événement programmé à annuler \(peut être l'URI complète ou simplement l'UUID\) |
|
||||
| `reason` | string | Non | Motif d'annulation \(sera envoyé aux invités\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Détails de l'annulation |
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `tools`
|
||||
- Type : `calendly`
|
||||
@@ -23,10 +23,12 @@ Ajoutez une nouvelle mémoire à la base de données ou complétez une mémoire
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `id` | string | Oui | Identifiant pour la mémoire. Si une mémoire avec cet ID existe déjà, les nouvelles données y seront ajoutées. |
|
||||
| `role` | string | Oui | Rôle pour la mémoire de l'agent \(user, assistant, ou system\) |
|
||||
| `content` | string | Oui | Contenu pour la mémoire de l'agent |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation (par ex., user-123, session-abc). Si une mémoire avec cet identifiant de conversation existe déjà pour ce bloc, le nouveau message y sera ajouté. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `role` | chaîne | Oui | Rôle pour la mémoire de l'agent (user, assistant, ou system) |
|
||||
| `content` | chaîne | Oui | Contenu pour la mémoire de l'agent |
|
||||
| `blockId` | chaîne | Non | ID de bloc optionnel. Si non fourni, utilise l'ID du bloc actuel du contexte d'exécution, ou par défaut "default". |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -38,22 +40,25 @@ Ajoutez une nouvelle mémoire à la base de données ou complétez une mémoire
|
||||
|
||||
### `memory_get`
|
||||
|
||||
Récupérer une mémoire spécifique par son identifiant
|
||||
Récupérer la mémoire par conversationId, blockId, blockName, ou une combinaison. Renvoie toutes les mémoires correspondantes.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `id` | string | Oui | Identifiant de la mémoire à récupérer |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation (par ex., user-123, session-abc). Si fourni seul, renvoie toutes les mémoires pour cette conversation à travers tous les blocs. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `blockId` | chaîne | Non | Identifiant de bloc. Si fourni seul, renvoie toutes les mémoires pour ce bloc à travers toutes les conversations. Si fourni avec conversationId, renvoie les mémoires pour cette conversation spécifique dans ce bloc. |
|
||||
| `blockName` | chaîne | Non | Nom du bloc. Alternative à blockId. Si fourni seul, renvoie toutes les mémoires pour les blocs avec ce nom. Si fourni avec conversationId, renvoie les mémoires pour cette conversation dans les blocs avec ce nom. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indique si la mémoire a été récupérée avec succès |
|
||||
| `memories` | array | Tableau de données de mémoire pour l'identifiant demandé |
|
||||
| `message` | string | Message de succès ou d'erreur |
|
||||
| `error` | string | Message d'erreur si l'opération a échoué |
|
||||
| `success` | booléen | Indique si la mémoire a été récupérée avec succès |
|
||||
| `memories` | tableau | Tableau d'objets de mémoire avec les champs conversationId, blockId, blockName et data |
|
||||
| `message` | chaîne | Message de succès ou d'erreur |
|
||||
| `error` | chaîne | Message d'erreur si l'opération a échoué |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -68,20 +73,23 @@ Récupérer toutes les mémoires de la base de données
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indique si toutes les mémoires ont été récupérées avec succès |
|
||||
| `memories` | array | Tableau de tous les objets de mémoire avec leurs clés, types et données |
|
||||
| `message` | string | Message de succès ou d'erreur |
|
||||
| `error` | string | Message d'erreur si l'opération a échoué |
|
||||
| `success` | booléen | Indique si toutes les mémoires ont été récupérées avec succès |
|
||||
| `memories` | tableau | Tableau de tous les objets de mémoire avec les champs key, conversationId, blockId, blockName et data |
|
||||
| `message` | chaîne | Message de succès ou d'erreur |
|
||||
| `error` | chaîne | Message d'erreur si l'opération a échoué |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
Supprimer un souvenir spécifique par son ID
|
||||
Supprimer des mémoires par conversationId, blockId, blockName, ou une combinaison. Prend en charge la suppression en masse.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `id` | chaîne | Oui | Identifiant du souvenir à supprimer |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `conversationId` | chaîne | Non | Identifiant de conversation \(par exemple, user-123, session-abc\). Si fourni seul, supprime toutes les mémoires pour cette conversation dans tous les blocs. |
|
||||
| `id` | chaîne | Non | Paramètre hérité pour l'identifiant de conversation. Utilisez conversationId à la place. Fourni pour la rétrocompatibilité. |
|
||||
| `blockId` | chaîne | Non | Identifiant de bloc. Si fourni seul, supprime toutes les mémoires pour ce bloc dans toutes les conversations. Si fourni avec conversationId, supprime les mémoires pour cette conversation spécifique dans ce bloc. |
|
||||
| `blockName` | chaîne | Non | Nom du bloc. Alternative à blockId. Si fourni seul, supprime toutes les mémoires pour les blocs avec ce nom. Si fourni avec conversationId, supprime les mémoires pour cette conversation dans les blocs avec ce nom. |
|
||||
|
||||
#### Sortie
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Dans Sim, l'intégration Microsoft Excel offre un accès transparent aux fonctio
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez Microsoft Excel dans le flux de travail. Peut lire, écrire, mettre à jour et ajouter au tableau. Nécessite OAuth.
|
||||
Intégrez Microsoft Excel dans le flux de travail. Peut lire, écrire, mettre à jour, ajouter au tableau et créer de nouvelles feuilles de calcul.
|
||||
|
||||
## Outils
|
||||
|
||||
@@ -91,6 +91,23 @@ Ajouter de nouvelles lignes à un tableau Microsoft Excel
|
||||
| `values` | array | Tableau des lignes qui ont été ajoutées au tableau |
|
||||
| `metadata` | object | Métadonnées de la feuille de calcul |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
Créer une nouvelle feuille de calcul dans un classeur Microsoft Excel
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | chaîne | Oui | L'identifiant du classeur Excel dans lequel ajouter la feuille de calcul |
|
||||
| `worksheetName` | chaîne | Oui | Le nom de la nouvelle feuille de calcul. Doit être unique dans le classeur et ne peut pas dépasser 31 caractères |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | objet | Détails de la feuille de calcul nouvellement créée |
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `tools`
|
||||
|
||||
172
apps/docs/content/docs/fr/tools/neo4j.mdx
Normal file
172
apps/docs/content/docs/fr/tools/neo4j.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Connexion à la base de données graphe Neo4j
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez la base de données graphe Neo4j dans le flux de travail. Permet d'interroger, créer, fusionner, mettre à jour et supprimer des nœuds et des relations.
|
||||
|
||||
## Outils
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
Exécutez des requêtes MATCH pour lire les nœuds et les relations de la base de données graphe Neo4j. Pour de meilleures performances et pour éviter les grands ensembles de résultats, incluez LIMIT dans votre requête (par exemple,
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | number | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | string | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | string | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | string | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | string | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Oui | Requête Cypher à exécuter \(généralement des instructions MATCH\) |
|
||||
| `parameters` | object | Non | Paramètres pour la requête Cypher sous forme d'objet JSON. À utiliser pour toutes les valeurs dynamiques, y compris LIMIT \(par exemple, query: "MATCH \(n\) RETURN n LIMIT $limit", parameters: \{limit: 100\}\). |
|
||||
| `parameters` | string | Non | Pas de description |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message d'état de l'opération |
|
||||
| `records` | array | Tableau des enregistrements retournés par la requête |
|
||||
| `recordCount` | number | Nombre d'enregistrements retournés |
|
||||
| `summary` | json | Résumé de l'exécution de la requête avec timing et compteurs |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
Exécuter des instructions CREATE pour ajouter de nouveaux nœuds et relations à la base de données graphique Neo4j
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `host` | chaîne | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | nombre | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | chaîne | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | chaîne | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | chaîne | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | chaîne | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | chaîne | Oui | Instruction Cypher CREATE à exécuter |
|
||||
| `parameters` | objet | Non | Paramètres pour la requête Cypher sous forme d'objet JSON |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | chaîne | Message d'état de l'opération |
|
||||
| `summary` | json | Résumé de création avec compteurs pour les nœuds et relations créés |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
Exécuter des instructions MERGE pour trouver ou créer des nœuds et relations dans Neo4j (opération d'upsert)
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `host` | chaîne | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | nombre | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | chaîne | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | chaîne | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | chaîne | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | chaîne | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | chaîne | Oui | Instruction Cypher MERGE à exécuter |
|
||||
| `parameters` | objet | Non | Paramètres pour la requête Cypher sous forme d'objet JSON |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message d'état de l'opération |
|
||||
| `summary` | json | Résumé de fusion avec compteurs pour les nœuds/relations créés ou correspondants |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Exécuter des instructions SET pour mettre à jour les propriétés des nœuds et relations existants dans Neo4j
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | number | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | string | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | string | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | string | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | string | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Oui | Requête Cypher avec instructions MATCH et SET pour mettre à jour les propriétés |
|
||||
| `parameters` | object | Non | Paramètres pour la requête Cypher sous forme d'objet JSON |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message d'état de l'opération |
|
||||
| `summary` | json | Résumé de mise à jour avec compteurs pour les propriétés définies |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Exécuter des instructions DELETE ou DETACH DELETE pour supprimer des nœuds et des relations de Neo4j
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | number | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | string | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | string | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | string | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | string | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Oui | Requête Cypher avec instructions MATCH et DELETE/DETACH DELETE |
|
||||
| `parameters` | object | Non | Paramètres pour la requête Cypher sous forme d'objet JSON |
|
||||
| `detach` | boolean | Non | Indique s'il faut utiliser DETACH DELETE pour supprimer les relations avant de supprimer les nœuds |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message d'état de l'opération |
|
||||
| `summary` | json | Résumé de suppression avec compteurs pour les nœuds et relations supprimés |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
Exécuter des requêtes Cypher arbitraires sur la base de données graphique Neo4j pour des opérations complexes
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `host` | string | Oui | Nom d'hôte ou adresse IP du serveur Neo4j |
|
||||
| `port` | number | Oui | Port du serveur Neo4j \(par défaut : 7687 pour le protocole Bolt\) |
|
||||
| `database` | string | Oui | Nom de la base de données à laquelle se connecter |
|
||||
| `username` | string | Oui | Nom d'utilisateur Neo4j |
|
||||
| `password` | string | Oui | Mot de passe Neo4j |
|
||||
| `encryption` | string | Non | Mode de chiffrement de connexion \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | Oui | Requête Cypher à exécuter \(toute instruction Cypher valide\) |
|
||||
| `parameters` | object | Non | Paramètres pour la requête Cypher sous forme d'objet JSON |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message d'état de l'opération |
|
||||
| `records` | array | Tableau des enregistrements retournés par la requête |
|
||||
| `recordCount` | number | Nombre d'enregistrements retournés |
|
||||
| `summary` | json | Résumé d'exécution avec chronométrage et compteurs |
|
||||
|
||||
## Notes
|
||||
|
||||
- Catégorie : `tools`
|
||||
- Type : `neo4j`
|
||||
117
apps/docs/content/docs/fr/tools/stt.mdx
Normal file
117
apps/docs/content/docs/fr/tools/stt.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Reconnaissance vocale
|
||||
description: Convertir la parole en texte à l'aide de l'IA
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Transcrivez la parole en texte en utilisant des modèles d'IA de pointe des principaux fournisseurs. Les outils Sim de reconnaissance vocale (STT) vous permettent de convertir des fichiers audio et vidéo en transcriptions précises, prenant en charge plusieurs langues, horodatages et traduction optionnelle.
|
||||
|
||||
Fournisseurs pris en charge :
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)** : Modèle STT open-source avancé d'OpenAI. Prend en charge des modèles tels que `whisper-1` et gère une grande variété de langues et de formats audio.
|
||||
- **[Deepgram](https://deepgram.com/)** : API STT en temps réel et par lots avec des modèles d'apprentissage profond comme `nova-3`, `nova-2` et `whisper-large`. Offre des fonctionnalités comme la diarisation, la reconnaissance d'intention et le réglage spécifique à l'industrie.
|
||||
- **[ElevenLabs](https://elevenlabs.io/)** : Connu pour l'IA vocale de haute qualité, ElevenLabs fournit des modèles STT axés sur la précision et la compréhension du langage naturel pour de nombreuses langues et dialectes.
|
||||
|
||||
Choisissez le fournisseur et le modèle les mieux adaptés à votre tâche — que ce soit pour une transcription rapide de qualité production (Deepgram), une capacité multilingue hautement précise (Whisper), ou une compréhension avancée et une couverture linguistique étendue (ElevenLabs).
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Transcrivez des fichiers audio et vidéo en texte à l'aide des principaux fournisseurs d'IA. Prend en charge plusieurs langues, horodatages et diarisation des locuteurs.
|
||||
|
||||
## Outils
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
Transcrire l'audio en texte avec OpenAI Whisper
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Oui | Fournisseur STT \(whisper\) |
|
||||
| `apiKey` | string | Oui | Clé API OpenAI |
|
||||
| `model` | string | Non | Modèle Whisper à utiliser \(par défaut : whisper-1\) |
|
||||
| `audioFile` | file | Non | Fichier audio ou vidéo à transcrire |
|
||||
| `audioFileReference` | file | Non | Référence au fichier audio/vidéo des blocs précédents |
|
||||
| `audioUrl` | string | Non | URL vers un fichier audio ou vidéo |
|
||||
| `language` | string | Non | Code de langue \(ex. "en", "es", "fr"\) ou "auto" pour la détection automatique |
|
||||
| `timestamps` | string | Non | Granularité des horodatages : none, sentence, ou word |
|
||||
| `translateToEnglish` | boolean | Non | Traduire l'audio en anglais |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Texte transcrit complet |
|
||||
| `segments` | array | Segments horodatés |
|
||||
| `language` | string | Langue détectée ou spécifiée |
|
||||
| `duration` | number | Durée audio en secondes |
|
||||
| `confidence` | number | Score de confiance global |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
Transcrire l'audio en texte en utilisant Deepgram
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | Oui | Fournisseur STT \(deepgram\) |
|
||||
| `apiKey` | string | Oui | Clé API Deepgram |
|
||||
| `model` | string | Non | Modèle Deepgram à utiliser \(nova-3, nova-2, whisper-large, etc.\) |
|
||||
| `audioFile` | file | Non | Fichier audio ou vidéo à transcrire |
|
||||
| `audioFileReference` | file | Non | Référence au fichier audio/vidéo des blocs précédents |
|
||||
| `audioUrl` | string | Non | URL vers un fichier audio ou vidéo |
|
||||
| `language` | string | Non | Code de langue \(ex. "en", "es", "fr"\) ou "auto" pour la détection automatique |
|
||||
| `timestamps` | string | Non | Granularité des horodatages : none, sentence, ou word |
|
||||
| `diarization` | boolean | Non | Activer la diarisation des locuteurs |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | Texte transcrit complet |
|
||||
| `segments` | array | Segments horodatés avec étiquettes de locuteurs |
|
||||
| `language` | string | Langue détectée ou spécifiée |
|
||||
| `duration` | number | Durée audio en secondes |
|
||||
| `confidence` | number | Score de confiance global |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
Transcrire l'audio en texte avec ElevenLabs
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `provider` | chaîne | Oui | Fournisseur STT \(elevenlabs\) |
|
||||
| `apiKey` | chaîne | Oui | Clé API ElevenLabs |
|
||||
| `model` | chaîne | Non | Modèle ElevenLabs à utiliser \(scribe_v1, scribe_v1_experimental\) |
|
||||
| `audioFile` | fichier | Non | Fichier audio ou vidéo à transcrire |
|
||||
| `audioFileReference` | fichier | Non | Référence au fichier audio/vidéo des blocs précédents |
|
||||
| `audioUrl` | chaîne | Non | URL vers un fichier audio ou vidéo |
|
||||
| `language` | chaîne | Non | Code de langue \(ex. "en", "es", "fr"\) ou "auto" pour la détection automatique |
|
||||
| `timestamps` | chaîne | Non | Granularité des horodatages : none, sentence, ou word |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | chaîne | Texte transcrit complet |
|
||||
| `segments` | tableau | Segments horodatés |
|
||||
| `language` | chaîne | Langue détectée ou spécifiée |
|
||||
| `duration` | nombre | Durée audio en secondes |
|
||||
| `confidence` | nombre | Score de confiance global |
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `tools`
|
||||
- Type : `stt`
|
||||
@@ -42,10 +42,10 @@ When responding to questions about investments, include risk disclaimers.
|
||||
|
||||
エージェントブロックは統一された推論インターフェースを通じて複数のLLMプロバイダーをサポートしています。利用可能なモデルには以下が含まれます:
|
||||
|
||||
- **OpenAI**: GPT-5、GPT-4o、o1、o3、o4-mini、gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **OpenAI**: GPT-5.1、GPT-5、GPT-4o、o1、o3、o4-mini、gpt-4.1
|
||||
- **Anthropic**: Claude 4.5 Sonnet、Claude Opus 4.1
|
||||
- **Google**: Gemini 2.5 Pro、Gemini 2.0 Flash
|
||||
- **その他のプロバイダー**: Groq、Cerebras、xAI、DeepSeek
|
||||
- **その他のプロバイダー**: Groq、Cerebras、xAI、Azure OpenAI、OpenRouter
|
||||
- **ローカルモデル**: Ollama互換モデル
|
||||
|
||||
### 温度
|
||||
|
||||
172
apps/docs/content/docs/ja/tools/calendly.mdx
Normal file
172
apps/docs/content/docs/ja/tools/calendly.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: Calendlyのスケジュール管理とイベント管理
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/)は、会議、イベント、予約を簡単に設定できる人気のスケジューリング自動化プラットフォームです。Calendlyを使用すると、チームや個人はスケジューリングを効率化し、メールのやり取りを減らし、イベント周りのタスクを自動化できます。
|
||||
|
||||
Sim Calendly統合により、エージェントは以下のことができます:
|
||||
|
||||
- **アカウントや予定されたイベントに関する情報を取得する**:ツールを使用してユーザー情報、イベントタイプ、予定されたイベントを分析や自動化のために取得します。
|
||||
- **イベントタイプとスケジューリングを管理する**:ユーザーや組織の利用可能なイベントタイプにアクセスしてリスト化し、特定のイベントタイプの詳細を取得し、予定されている会議や招待者データを監視します。
|
||||
- **フォローアップとワークフローを自動化する**:ユーザーが会議をスケジュール、再スケジュール、またはキャンセルした場合、Simエージェントは自動的に対応するワークフロー(リマインダーの送信、CRMの更新、参加者への通知など)をトリガーできます。
|
||||
- **webhookを使用して簡単に統合する**:招待者がスケジュール、キャンセル、またはルーティングフォームとやり取りする場合など、リアルタイムのCalendly webhookイベントに応答するSimワークフローを設定します。
|
||||
|
||||
会議の準備を自動化したい場合でも、招待を管理したい場合でも、スケジューリングアクティビティに応じてカスタムワークフローを実行したい場合でも、SimのCalendlyツールは柔軟で安全なアクセスを提供します。スケジュール変更に即座に反応する新しい自動化を解放し、チームの運用とコミュニケーションを効率化します。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用方法
|
||||
|
||||
Calendlyをワークフローに統合します。イベントタイプ、予定されたイベント、招待者、webhookを管理します。Calendly webhookイベント(招待者がスケジュール、招待者がキャンセル、ルーティングフォームが送信された)に基づいてワークフローをトリガーすることもできます。個人アクセストークンが必要です。
|
||||
|
||||
## ツール
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
現在認証されているCalendlyユーザーに関する情報を取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | タイプ | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | 現在のユーザー情報 |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
ユーザーまたは組織のすべてのイベントタイプのリストを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `user` | string | いいえ | このユーザーに属するイベントタイプのみを返す(URI形式) |
|
||||
| `organization` | string | いいえ | この組織に属するイベントタイプのみを返す(URI形式) |
|
||||
| `count` | number | いいえ | ページあたりの結果数(デフォルト:20、最大:100) |
|
||||
| `pageToken` | string | いいえ | ページネーション用のページトークン |
|
||||
| `sort` | string | いいえ | 結果のソート順(例:「name:asc」、「name:desc」) |
|
||||
| `active` | boolean | いいえ | trueの場合、アクティブなイベントタイプのみを表示。falseまたはチェックされていない場合、すべてのイベントタイプ(アクティブと非アクティブの両方)を表示。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | イベントタイプオブジェクトの配列 |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
特定のイベントタイプに関する詳細情報を取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `eventTypeUuid` | string | はい | イベントタイプUUID(完全なURIまたはUUIDのみ) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | イベントタイプの詳細 |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
ユーザーまたは組織の予定されたイベントのリストを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `user` | string | いいえ | このユーザーに属するイベントを返します(URI形式)。「ユーザー」または「組織」のいずれかを提供する必要があります。 |
|
||||
| `organization` | string | いいえ | この組織に属するイベントを返します(URI形式)。「ユーザー」または「組織」のいずれかを提供する必要があります。 |
|
||||
| `invitee_email` | string | いいえ | 招待者がこのメールアドレスを持つイベントを返します |
|
||||
| `count` | number | いいえ | ページあたりの結果数(デフォルト:20、最大:100) |
|
||||
| `max_start_time` | string | いいえ | この時間より前に開始時間があるイベントを返します(ISO 8601形式) |
|
||||
| `min_start_time` | string | いいえ | この時間より後に開始時間があるイベントを返します(ISO 8601形式) |
|
||||
| `pageToken` | string | いいえ | ページネーション用のページトークン |
|
||||
| `sort` | string | いいえ | 結果の並べ替え順序(例:"start_time:asc"、"start_time:desc") |
|
||||
| `status` | string | いいえ | ステータスでフィルタリング("active"または"canceled") |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | 予定されたイベントオブジェクトの配列 |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
特定のスケジュールされたイベントに関する詳細情報を取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `eventUuid` | string | はい | スケジュールされたイベントUUID(完全なURIまたはUUIDのみ) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | スケジュールされたイベントの詳細 |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
スケジュールされたイベントの招待者リストを取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `eventUuid` | string | はい | スケジュールされたイベントUUID(完全なURIまたはUUIDのみ) |
|
||||
| `count` | number | いいえ | ページあたりの結果数(デフォルト:20、最大:100) |
|
||||
| `email` | string | いいえ | メールアドレスで招待者をフィルタリング |
|
||||
| `pageToken` | string | いいえ | ページネーション用のページトークン |
|
||||
| `sort` | string | いいえ | 結果のソート順(例:"created_at:asc"、"created_at:desc") |
|
||||
| `status` | string | いいえ | ステータスによるフィルタリング("active"または"canceled") |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | 招待者オブジェクトの配列 |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
スケジュールされたイベントをキャンセルする
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | はい | Calendly個人アクセストークン |
|
||||
| `eventUuid` | string | はい | キャンセルするスケジュールされたイベントのUUID(完全なURIまたはUUIDのみ) |
|
||||
| `reason` | string | いいえ | キャンセルの理由(招待者に送信されます) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | キャンセルの詳細 |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリー: `tools`
|
||||
- タイプ: `calendly`
|
||||
@@ -24,9 +24,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | はい | メモリーの識別子。このIDのメモリーが既に存在する場合、新しいデータはそれに追加されます。 |
|
||||
| `role` | string | はい | エージェントメモリーの役割(user、assistant、またはsystem) |
|
||||
| `content` | string | はい | エージェントメモリーのコンテンツ |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。このブロックに対してこの会話IDのメモリがすでに存在する場合、新しいメッセージはそれに追加されます。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `role` | string | はい | エージェントメモリの役割(user、assistant、またはsystem) |
|
||||
| `content` | string | はい | エージェントメモリのコンテンツ |
|
||||
| `blockId` | string | いいえ | オプションのブロックID。提供されない場合、実行コンテキストから現在のブロックIDを使用するか、デフォルトで「default」になります。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -38,20 +40,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
### `memory_get`
|
||||
|
||||
IDで特定のメモリを取得する
|
||||
conversationId、blockId、blockName、またはそれらの組み合わせによってメモリを取得します。一致するすべてのメモリを返します。
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | はい | 取得するメモリの識別子 |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。単独で提供された場合、すべてのブロックにわたるこの会話のすべてのメモリを返します。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `blockId` | string | いいえ | ブロック識別子。単独で提供された場合、すべての会話にわたるこのブロックのすべてのメモリを返します。conversationIdと一緒に提供された場合、そのブロック内の特定の会話のメモリを返します。 |
|
||||
| `blockName` | string | いいえ | ブロック名。blockIdの代替。単独で提供された場合、この名前を持つブロックのすべてのメモリを返します。conversationIdと一緒に提供された場合、この名前を持つブロック内のその会話のメモリを返します。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | メモリが正常に取得されたかどうか |
|
||||
| `memories` | array | リクエストされたIDのメモリデータの配列 |
|
||||
| `memories` | array | conversationId、blockId、blockName、およびdataフィールドを含むメモリオブジェクトの配列 |
|
||||
| `message` | string | 成功またはエラーメッセージ |
|
||||
| `error` | string | 操作が失敗した場合のエラーメッセージ |
|
||||
|
||||
@@ -69,19 +74,22 @@ IDで特定のメモリを取得する
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | すべてのメモリが正常に取得されたかどうか |
|
||||
| `memories` | array | キー、型、データを含むすべてのメモリオブジェクトの配列 |
|
||||
| `memories` | array | key、conversationId、blockId、blockName、およびdataフィールドを含むすべてのメモリオブジェクトの配列 |
|
||||
| `message` | string | 成功またはエラーメッセージ |
|
||||
| `error` | string | 操作が失敗した場合のエラーメッセージ |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
IDで特定のメモリを削除する
|
||||
conversationId、blockId、blockName、またはそれらの組み合わせによってメモリを削除します。一括削除をサポートしています。
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | はい | 削除するメモリの識別子 |
|
||||
| `conversationId` | string | いいえ | 会話識別子(例:user-123、session-abc)。単独で提供された場合、すべてのブロックにわたるこの会話のすべてのメモリを削除します。 |
|
||||
| `id` | string | いいえ | 会話識別子のレガシーパラメータ。代わりにconversationIdを使用してください。後方互換性のために提供されています。 |
|
||||
| `blockId` | string | いいえ | ブロック識別子。単独で提供された場合、すべての会話にわたるこのブロックのすべてのメモリを削除します。conversationIdと共に提供された場合、そのブロック内の特定の会話のメモリを削除します。 |
|
||||
| `blockName` | string | いいえ | ブロック名。blockIdの代替。単独で提供された場合、この名前を持つブロックのすべてのメモリを削除します。conversationIdと共に提供された場合、この名前を持つブロック内のその会話のメモリを削除します。 |
|
||||
|
||||
#### 出力
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Simでは、Microsoft Excel統合によりOAuth認証を通じてスプレッド
|
||||
|
||||
## 使用方法
|
||||
|
||||
Microsoft Excelをワークフローに統合します。テーブルの読み取り、書き込み、更新、追加が可能です。OAuthが必要です。
|
||||
Microsoft Excelをワークフローに統合します。読み取り、書き込み、更新、テーブルへの追加、新しいワークシートの作成が可能です。
|
||||
|
||||
## ツール
|
||||
|
||||
@@ -91,6 +91,23 @@ Microsoft Excelテーブルに新しい行を追加する
|
||||
| `values` | array | テーブルに追加された行の配列 |
|
||||
| `metadata` | object | スプレッドシートのメタデータ |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
Microsoft Excelブックに新しいワークシート(シート)を作成する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | はい | ワークシートを追加するExcelブックのID |
|
||||
| `worksheetName` | string | はい | 新しいワークシートの名前。ブック内で一意である必要があり、31文字を超えることはできません |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | object | 新しく作成されたワークシートの詳細 |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリー: `tools`
|
||||
|
||||
172
apps/docs/content/docs/ja/tools/neo4j.mdx
Normal file
172
apps/docs/content/docs/ja/tools/neo4j.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: Neo4jグラフデータベースに接続する
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## 使用方法
|
||||
|
||||
Neo4jグラフデータベースをワークフローに統合します。ノードとリレーションシップのクエリ、作成、マージ、更新、削除が可能です。
|
||||
|
||||
## ツール
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
MATCHクエリを実行してNeo4jグラフデータベースからノードとリレーションシップを読み取ります。最適なパフォーマンスを得るため、また大きな結果セットを防ぐために、クエリにLIMITを含めてください(例:
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーのポート(デフォルト:Boltプロトコル用に7687) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jユーザー名 |
|
||||
| `password` | string | はい | Neo4jパスワード |
|
||||
| `encryption` | string | いいえ | 接続暗号化モード(enabled、disabled) |
|
||||
| `cypherQuery` | string | はい | 実行するCypherクエリ(通常はMATCH文) |
|
||||
| `parameters` | object | いいえ | CypherクエリのパラメータをJSONオブジェクトとして指定。LIMITを含む動的な値に使用します(例:query: "MATCH \(n\) RETURN n LIMIT $limit"、parameters: \{limit: 100\}) |
|
||||
| `parameters` | string | いいえ | 説明なし |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作ステータスメッセージ |
|
||||
| `records` | array | クエリから返されたレコードの配列 |
|
||||
| `recordCount` | number | 返されたレコード数 |
|
||||
| `summary` | json | タイミングとカウンターを含むクエリ実行の概要 |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
CREATE文を実行してNeo4jグラフデータベースに新しいノードとリレーションシップを追加する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーのポート番号 \(デフォルト: Boltプロトコル用に7687\) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jのユーザー名 |
|
||||
| `password` | string | はい | Neo4jのパスワード |
|
||||
| `encryption` | string | いいえ | 接続の暗号化モード \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | はい | 実行するCypher CREATE文 |
|
||||
| `parameters` | object | いいえ | CypherクエリのパラメータをJSONオブジェクトとして指定 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作のステータスメッセージ |
|
||||
| `summary` | json | 作成されたノードとリレーションシップの数を含む作成サマリー |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
MERGE文を実行してNeo4jでノードとリレーションシップを検索または作成する(アップサート操作)
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーのポート番号 \(デフォルト: Boltプロトコル用に7687\) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jのユーザー名 |
|
||||
| `password` | string | はい | Neo4jのパスワード |
|
||||
| `encryption` | string | いいえ | 接続の暗号化モード \(enabled, disabled\) |
|
||||
| `cypherQuery` | string | はい | 実行するCypher MERGE文 |
|
||||
| `parameters` | object | いいえ | CypherクエリのパラメータをJSONオブジェクトとして指定 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作ステータスメッセージ |
|
||||
| `summary` | json | 作成またはマッチしたノード/リレーションシップのカウンターを含むマージ概要 |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
Neo4jの既存ノードとリレーションシップのプロパティを更新するためのSETステートメントを実行します
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーポート(デフォルト:Boltプロトコル用7687) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jユーザー名 |
|
||||
| `password` | string | はい | Neo4jパスワード |
|
||||
| `encryption` | string | いいえ | 接続暗号化モード(enabled、disabled) |
|
||||
| `cypherQuery` | string | はい | プロパティを更新するためのMATCHとSETステートメントを含むCypherクエリ |
|
||||
| `parameters` | object | いいえ | JSONオブジェクトとしてのCypherクエリのパラメータ |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作ステータスメッセージ |
|
||||
| `summary` | json | 設定されたプロパティのカウンターを含む更新概要 |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
Neo4jからノードとリレーションシップを削除するためのDELETEまたはDETACH DELETEステートメントを実行します
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーポート(デフォルト:Boltプロトコル用7687) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jユーザー名 |
|
||||
| `password` | string | はい | Neo4jパスワード |
|
||||
| `encryption` | string | いいえ | 接続暗号化モード(enabled、disabled) |
|
||||
| `cypherQuery` | string | はい | MATCHとDELETE/DETACH DELETEステートメントを含むCypherクエリ |
|
||||
| `parameters` | object | いいえ | JSONオブジェクトとしてのCypherクエリのパラメータ |
|
||||
| `detach` | boolean | いいえ | ノードを削除する前にリレーションシップを削除するためにDETACH DELETEを使用するかどうか |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作ステータスメッセージ |
|
||||
| `summary` | json | 削除されたノードとリレーションシップのカウンターを含む削除サマリー |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
複雑な操作のためにNeo4jグラフデータベースで任意のCypherクエリを実行する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | はい | Neo4jサーバーのホスト名またはIPアドレス |
|
||||
| `port` | number | はい | Neo4jサーバーポート(デフォルト:Boltプロトコル用の7687) |
|
||||
| `database` | string | はい | 接続先のデータベース名 |
|
||||
| `username` | string | はい | Neo4jユーザー名 |
|
||||
| `password` | string | はい | Neo4jパスワード |
|
||||
| `encryption` | string | いいえ | 接続暗号化モード(enabled、disabled) |
|
||||
| `cypherQuery` | string | はい | 実行するCypherクエリ(任意の有効なCypher文) |
|
||||
| `parameters` | object | いいえ | JSONオブジェクトとしてのCypherクエリのパラメータ |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作ステータスメッセージ |
|
||||
| `records` | array | クエリから返されたレコードの配列 |
|
||||
| `recordCount` | number | 返されたレコードの数 |
|
||||
| `summary` | json | タイミングとカウンターを含む実行サマリー |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリ: `tools`
|
||||
- タイプ: `neo4j`
|
||||
117
apps/docs/content/docs/ja/tools/stt.mdx
Normal file
117
apps/docs/content/docs/ja/tools/stt.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: 音声テキスト変換
|
||||
description: AIを使用して音声をテキストに変換
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
最先端のAIモデルを使用して音声をテキストに変換します。Sim音声テキスト変換(STT)ツールを使用すると、音声ファイルや動画ファイルを正確な文字起こしに変換でき、複数の言語、タイムスタンプ、およびオプションの翻訳をサポートしています。
|
||||
|
||||
対応プロバイダー:
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)**: OpenAIによる先進的なオープンソースSTTモデル。`whisper-1`などのモデルをサポートし、様々な言語と音声フォーマットに対応しています。
|
||||
- **[Deepgram](https://deepgram.com/)**: `nova-3`、`nova-2`、`whisper-large`などのディープラーニングモデルを使用したリアルタイムおよびバッチSTT API。話者分離、意図認識、業界特化型チューニングなどの機能を提供します。
|
||||
- **[ElevenLabs](https://elevenlabs.io/)**: 高品質な音声AIで知られるElevenLabsは、多数の言語や方言に対応した精度と自然言語理解に焦点を当てたSTTモデルを提供しています。
|
||||
|
||||
タスクに最適なプロバイダーとモデルを選択してください—高速で本番環境向けの文字起こし(Deepgram)、高精度の多言語対応(Whisper)、または高度な理解と言語カバレッジ(ElevenLabs)など。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用方法
|
||||
|
||||
主要なAIプロバイダーを使用して音声ファイルや動画ファイルをテキストに変換します。複数の言語、タイムスタンプ、および話者分離をサポートしています。
|
||||
|
||||
## ツール
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
OpenAI Whisperを使用して音声をテキストに変換
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | はい | STTプロバイダー(whisper) |
|
||||
| `apiKey` | string | はい | OpenAI APIキー |
|
||||
| `model` | string | いいえ | 使用するWhisperモデル(デフォルト:whisper-1) |
|
||||
| `audioFile` | file | いいえ | 文字起こしする音声または動画ファイル |
|
||||
| `audioFileReference` | file | いいえ | 前のブロックからの音声/動画ファイルへの参照 |
|
||||
| `audioUrl` | string | いいえ | 音声または動画ファイルのURL |
|
||||
| `language` | string | いいえ | 言語コード(例:"en"、"es"、"fr")または自動検出の場合は"auto" |
|
||||
| `timestamps` | string | いいえ | タイムスタンプの粒度:none、sentence、またはword |
|
||||
| `translateToEnglish` | boolean | いいえ | 音声を英語に翻訳 |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完全な文字起こしテキスト |
|
||||
| `segments` | array | タイムスタンプ付きセグメント |
|
||||
| `language` | string | 検出または指定された言語 |
|
||||
| `duration` | number | 音声の長さ(秒) |
|
||||
| `confidence` | number | 全体的な信頼度スコア |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
Deepgramを使用して音声をテキストに文字起こし
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | はい | STTプロバイダー(deepgram) |
|
||||
| `apiKey` | string | はい | Deepgram APIキー |
|
||||
| `model` | string | いいえ | 使用するDeepgramモデル(nova-3、nova-2、whisper-largeなど) |
|
||||
| `audioFile` | file | いいえ | 文字起こしする音声またはビデオファイル |
|
||||
| `audioFileReference` | file | いいえ | 前のブロックからの音声/ビデオファイルの参照 |
|
||||
| `audioUrl` | string | いいえ | 音声またはビデオファイルのURL |
|
||||
| `language` | string | いいえ | 言語コード(例:"en"、"es"、"fr")または自動検出の場合は"auto" |
|
||||
| `timestamps` | string | いいえ | タイムスタンプの粒度:none、sentence、またはword |
|
||||
| `diarization` | boolean | いいえ | 話者分離を有効にする |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完全な文字起こしテキスト |
|
||||
| `segments` | array | 話者ラベル付きのタイムスタンプセグメント |
|
||||
| `language` | string | 検出または指定された言語 |
|
||||
| `duration` | number | 音声の長さ(秒) |
|
||||
| `confidence` | number | 全体的な信頼度スコア |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
ElevenLabsを使用して音声をテキストに変換する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | はい | STTプロバイダー(elevenlabs) |
|
||||
| `apiKey` | string | はい | ElevenLabs APIキー |
|
||||
| `model` | string | いいえ | 使用するElevenLabsモデル(scribe_v1, scribe_v1_experimental) |
|
||||
| `audioFile` | file | いいえ | 文字起こしする音声またはビデオファイル |
|
||||
| `audioFileReference` | file | いいえ | 前のブロックからの音声/ビデオファイルの参照 |
|
||||
| `audioUrl` | string | いいえ | 音声またはビデオファイルのURL |
|
||||
| `language` | string | いいえ | 言語コード(例:"en"、"es"、"fr")または自動検出の場合は"auto" |
|
||||
| `timestamps` | string | いいえ | タイムスタンプの粒度:none、sentence、またはword |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完全な文字起こしテキスト |
|
||||
| `segments` | array | タイムスタンプ付きセグメント |
|
||||
| `language` | string | 検出または指定された言語 |
|
||||
| `duration` | number | 音声の長さ(秒) |
|
||||
| `confidence` | number | 全体的な信頼度スコア |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリー: `tools`
|
||||
- タイプ: `stt`
|
||||
@@ -42,10 +42,10 @@ When responding to questions about investments, include risk disclaimers.
|
||||
|
||||
代理模块通过统一的推理接口支持多个 LLM 提供商。可用模型包括:
|
||||
|
||||
- **OpenAI**:GPT-5、GPT-4o、o1、o3、o4-mini、gpt-4.1
|
||||
- **Anthropic**:Claude 3.7 Sonnet
|
||||
- **OpenAI**:GPT-5.1、GPT-5、GPT-4o、o1、o3、o4-mini、gpt-4.1
|
||||
- **Anthropic**:Claude 4.5 Sonnet、Claude Opus 4.1
|
||||
- **Google**:Gemini 2.5 Pro、Gemini 2.0 Flash
|
||||
- **其他提供商**:Groq、Cerebras、xAI、DeepSeek
|
||||
- **其他提供商**:Groq、Cerebras、xAI、Azure OpenAI、OpenRouter
|
||||
- **本地模型**:兼容 Ollama 的模型
|
||||
|
||||
### 温度
|
||||
|
||||
172
apps/docs/content/docs/zh/tools/calendly.mdx
Normal file
172
apps/docs/content/docs/zh/tools/calendly.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Calendly
|
||||
description: 管理 Calendly 的日程安排和事件
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="calendly"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Calendly](https://calendly.com/) 是一个流行的日程安排自动化平台,可以帮助您轻松预订会议、活动和预约。通过 Calendly,团队和个人可以简化日程安排,减少来回邮件,并自动化与活动相关的任务。
|
||||
|
||||
通过 Sim 的 Calendly 集成,您的代理可以:
|
||||
|
||||
- **检索有关您的账户和已安排事件的信息**:使用工具获取用户信息、事件类型和已安排事件,以便进行分析或自动化。
|
||||
- **管理事件类型和日程安排**:访问并列出用户或组织的可用事件类型,检索特定事件类型的详细信息,并监控已安排的会议和受邀者数据。
|
||||
- **自动化跟进和工作流程**:当用户安排、重新安排或取消会议时,Sim 代理可以自动触发相应的工作流程,例如发送提醒、更新 CRM 或通知参与者。
|
||||
- **通过 Webhook 轻松集成**:设置 Sim 工作流程以响应实时 Calendly Webhook 事件,包括当受邀者安排、取消或与路由表单交互时。
|
||||
|
||||
无论您是想自动化会议准备、管理邀请,还是根据日程安排活动运行自定义工作流程,Sim 中的 Calendly 工具都为您提供灵活且安全的访问权限。通过即时响应日程安排的变化,解锁新的自动化功能——简化您的团队运营和沟通。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用说明
|
||||
|
||||
将 Calendly 集成到您的工作流程中。管理事件类型、已安排事件、受邀者和 Webhook。还可以基于 Calendly Webhook 事件(受邀者已安排、受邀者已取消、路由表单已提交)触发工作流程。需要个人访问令牌。
|
||||
|
||||
## 工具
|
||||
|
||||
### `calendly_get_current_user`
|
||||
|
||||
获取当前已认证的 Calendly 用户的信息
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | 当前用户信息 |
|
||||
|
||||
### `calendly_list_event_types`
|
||||
|
||||
检索用户或组织的所有事件类型列表
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `user` | string | 否 | 仅返回属于该用户的事件类型(URI 格式) |
|
||||
| `organization` | string | 否 | 仅返回属于该组织的事件类型(URI 格式) |
|
||||
| `count` | number | 否 | 每页结果数量(默认:20,最大:100) |
|
||||
| `pageToken` | string | 否 | 分页的页面令牌 |
|
||||
| `sort` | string | 否 | 结果的排序顺序(例如,“name:asc”、“name:desc”) |
|
||||
| `active` | boolean | 否 | 如果为 true,仅显示活动的事件类型。如果为 false 或未选中,则显示所有事件类型(包括活动和非活动)。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | 事件类型对象数组 |
|
||||
|
||||
### `calendly_get_event_type`
|
||||
|
||||
获取特定事件类型的详细信息
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `eventTypeUuid` | string | 是 | 事件类型 UUID(可以是完整 URI 或仅是 UUID) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | 事件类型详情 |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
|
||||
检索用户或组织的已安排事件列表
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `user` | string | 否 | 返回属于该用户的事件 \(URI 格式\)。必须提供 "user" 或 "organization" 之一。 |
|
||||
| `organization` | string | 否 | 返回属于该组织的事件 \(URI 格式\)。必须提供 "user" 或 "organization" 之一。 |
|
||||
| `invitee_email` | string | 否 | 返回受邀者具有此电子邮件的事件 |
|
||||
| `count` | number | 否 | 每页结果数量 \(默认: 20, 最大: 100\) |
|
||||
| `max_start_time` | string | 否 | 返回开始时间早于此时间的事件 \(ISO 8601 格式\) |
|
||||
| `min_start_time` | string | 否 | 返回开始时间晚于此时间的事件 \(ISO 8601 格式\) |
|
||||
| `pageToken` | string | 否 | 分页的页面令牌 |
|
||||
| `sort` | string | 否 | 结果的排序顺序 \(例如: "start_time:asc", "start_time:desc"\) |
|
||||
| `status` | string | 否 | 按状态筛选 \("active" 或 "canceled"\) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | 已安排事件对象的数组 |
|
||||
|
||||
### `calendly_get_scheduled_event`
|
||||
|
||||
获取特定计划事件的详细信息
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `eventUuid` | string | 是 | 计划事件 UUID(可以是完整 URI 或仅是 UUID) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | 计划事件详情 |
|
||||
|
||||
### `calendly_list_event_invitees`
|
||||
|
||||
检索计划事件的受邀者列表
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `eventUuid` | string | 是 | 计划事件 UUID(可以是完整 URI 或仅是 UUID) |
|
||||
| `count` | number | 否 | 每页结果数量(默认: 20,最大: 100) |
|
||||
| `email` | string | 否 | 按电子邮件地址筛选受邀者 |
|
||||
| `pageToken` | string | 否 | 分页的页面令牌 |
|
||||
| `sort` | string | 否 | 结果的排序顺序(例如: "created_at:asc", "created_at:desc") |
|
||||
| `status` | string | 否 | 按状态筛选("active" 或 "canceled") |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | 受邀者对象数组 |
|
||||
|
||||
### `calendly_cancel_event`
|
||||
|
||||
取消计划事件
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | 是 | Calendly 个人访问令牌 |
|
||||
| `eventUuid` | string | 是 | 要取消的计划事件 UUID \(可以是完整 URI 或仅是 UUID\) |
|
||||
| `reason` | string | 否 | 取消原因 \(将发送给受邀者\) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | 取消详情 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 类别: `tools`
|
||||
- 类型: `calendly`
|
||||
@@ -24,9 +24,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | 是 | 内存的标识符。如果具有此 ID 的内存已存在,新数据将追加到其中。 |
|
||||
| `role` | string | 是 | 代理内存的角色 \(用户、助手或系统\) |
|
||||
| `conversationId` | string | 否 | 会话标识符(例如,user-123,session-abc)。如果此 block 已存在具有该 conversationId 的内存,新消息将附加到该内存中。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `role` | string | 是 | 代理内存的角色(user、assistant 或 system) |
|
||||
| `content` | string | 是 | 代理内存的内容 |
|
||||
| `blockId` | string | 否 | 可选的 block ID。如果未提供,将使用执行上下文中的当前 block ID,或默认为 "default"。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -38,22 +40,25 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
### `memory_get`
|
||||
|
||||
通过 ID 检索特定内存
|
||||
通过 conversationId、blockId、blockName 或其组合检索内存。返回所有匹配的内存。
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | 字符串 | 是 | 要检索的内存标识符 |
|
||||
| `conversationId` | string | 否 | 会话标识符(例如,user-123,session-abc)。如果单独提供,将返回此会话在所有 block 中的所有内存。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `blockId` | string | 否 | block 标识符。如果单独提供,将返回此 block 中所有会话的所有内存。如果与 conversationId 一起提供,将返回此 block 中该特定会话的内存。 |
|
||||
| `blockName` | string | 否 | block 名称。blockId 的替代选项。如果单独提供,将返回具有此名称的 block 的所有内存。如果与 conversationId 一起提供,将返回具有此名称的 block 中该会话的内存。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | 布尔值 | 是否成功检索到内存 |
|
||||
| `memories` | 数组 | 请求 ID 的内存数据数组 |
|
||||
| `message` | 字符串 | 成功或错误信息 |
|
||||
| `error` | 字符串 | 如果操作失败,显示错误信息 |
|
||||
| `success` | boolean | 内存是否成功检索 |
|
||||
| `memories` | array | 包含 conversationId、blockId、blockName 和 data 字段的内存对象数组 |
|
||||
| `message` | string | 成功或错误信息 |
|
||||
| `error` | string | 如果操作失败的错误信息 |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -68,20 +73,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | 布尔值 | 是否成功检索到所有内存 |
|
||||
| `memories` | 数组 | 包含键、类型和数据的所有内存对象数组 |
|
||||
| `message` | 字符串 | 成功或错误信息 |
|
||||
| `error` | 字符串 | 如果操作失败,显示错误信息 |
|
||||
| `success` | boolean | 是否成功检索到所有内存 |
|
||||
| `memories` | array | 包含 key、conversationId、blockId、blockName 和 data 字段的所有内存对象数组 |
|
||||
| `message` | string | 成功或错误信息 |
|
||||
| `error` | string | 如果操作失败的错误信息 |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
通过其 ID 删除特定的内存
|
||||
通过 conversationId、blockId、blockName 或其组合删除内存。支持批量删除。
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | string | 是 | 要删除的内存标识符 |
|
||||
| `conversationId` | string | 否 | 会话标识符 \(例如,user-123,session-abc\)。如果单独提供,将删除此会话在所有块中的所有内存。 |
|
||||
| `id` | string | 否 | 会话标识符的旧参数。请改用 conversationId。为向后兼容而提供。 |
|
||||
| `blockId` | string | 否 | 块标识符。如果单独提供,将删除此块中所有会话的所有内存。如果与 conversationId 一起提供,将删除此块中特定会话的内存。 |
|
||||
| `blockName` | string | 否 | 块名称。是 blockId 的替代项。如果单独提供,将删除具有此名称的块的所有内存。如果与 conversationId 一起提供,将删除此名称的块中该会话的内存。 |
|
||||
|
||||
#### 输出
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
## 使用说明
|
||||
|
||||
将 Microsoft Excel 集成到工作流程中。可以读取、写入、更新和添加到表格中。需要 OAuth。
|
||||
将 Microsoft Excel 集成到工作流程中。可以读取、写入、更新、添加到表格以及创建新工作表。
|
||||
|
||||
## 工具
|
||||
|
||||
@@ -91,6 +91,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| `values` | array | 添加到表格的行数组 |
|
||||
| `metadata` | object | 电子表格元数据 |
|
||||
|
||||
### `microsoft_excel_worksheet_add`
|
||||
|
||||
在 Microsoft Excel 工作簿中创建一个新工作表
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | 是 | 要添加工作表的 Excel 工作簿的 ID |
|
||||
| `worksheetName` | string | 是 | 新工作表的名称。必须在工作簿中唯一,且不能超过 31 个字符 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `worksheet` | object | 新创建工作表的详细信息 |
|
||||
|
||||
## 注意
|
||||
|
||||
- 类别:`tools`
|
||||
|
||||
172
apps/docs/content/docs/zh/tools/neo4j.mdx
Normal file
172
apps/docs/content/docs/zh/tools/neo4j.mdx
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: Neo4j
|
||||
description: 连接到 Neo4j 图数据库
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="neo4j"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
## 使用说明
|
||||
|
||||
将 Neo4j 图数据库集成到工作流程中。可以查询、创建、合并、更新和删除节点及关系。
|
||||
|
||||
## 工具
|
||||
|
||||
### `neo4j_query`
|
||||
|
||||
执行 MATCH 查询以从 Neo4j 图数据库中读取节点和关系。为了获得最佳性能并防止结果集过大,请在查询中包含 LIMIT(例如,
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 要执行的 Cypher 查询 \(通常是 MATCH 语句\) |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象。用于任何动态值,包括 LIMIT \(例如,查询:"MATCH \(n\) RETURN n LIMIT $limit", 参数:\{limit: 100\}\)。 |
|
||||
| `parameters` | string | 否 | 无描述 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `records` | array | 查询返回的记录数组 |
|
||||
| `recordCount` | number | 返回的记录数量 |
|
||||
| `summary` | json | 查询执行摘要,包括时间和计数器 |
|
||||
|
||||
### `neo4j_create`
|
||||
|
||||
执行 CREATE 语句以向 Neo4j 图数据库添加新节点和关系
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 要执行的 Cypher CREATE 语句 |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `summary` | json | 创建摘要,包括创建的节点和关系的计数 |
|
||||
|
||||
### `neo4j_merge`
|
||||
|
||||
执行 MERGE 语句以在 Neo4j 中查找或创建节点和关系(插入或更新操作)
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 要执行的 Cypher MERGE 语句 |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `summary` | json | 包含节点/关系创建或匹配计数的合并摘要 |
|
||||
|
||||
### `neo4j_update`
|
||||
|
||||
执行 SET 语句以更新 Neo4j 中现有节点和关系的属性
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 包含 MATCH 和 SET 语句的 Cypher 查询以更新属性 |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `summary` | json | 包含已设置属性计数的更新摘要 |
|
||||
|
||||
### `neo4j_delete`
|
||||
|
||||
执行 DELETE 或 DETACH DELETE 语句以从 Neo4j 中删除节点和关系
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 包含 MATCH 和 DELETE/DETACH DELETE 语句的 Cypher 查询 |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象 |
|
||||
| `detach` | boolean | 否 | 是否使用 DETACH DELETE 在删除节点前移除关系 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `summary` | json | 删除摘要,包括已删除的节点和关系的计数 |
|
||||
|
||||
### `neo4j_execute`
|
||||
|
||||
在 Neo4j 图数据库上执行任意 Cypher 查询以进行复杂操作
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `host` | string | 是 | Neo4j 服务器主机名或 IP 地址 |
|
||||
| `port` | number | 是 | Neo4j 服务器端口 \(默认:Bolt 协议为 7687\) |
|
||||
| `database` | string | 是 | 要连接的数据库名称 |
|
||||
| `username` | string | 是 | Neo4j 用户名 |
|
||||
| `password` | string | 是 | Neo4j 密码 |
|
||||
| `encryption` | string | 否 | 连接加密模式 \(启用,禁用\) |
|
||||
| `cypherQuery` | string | 是 | 要执行的 Cypher 查询 \(任何有效的 Cypher 语句\) |
|
||||
| `parameters` | object | 否 | Cypher 查询的参数,格式为 JSON 对象 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | 操作状态消息 |
|
||||
| `records` | array | 查询返回的记录数组 |
|
||||
| `recordCount` | number | 返回的记录数量 |
|
||||
| `summary` | json | 执行摘要,包括时间和计数 |
|
||||
|
||||
## 注意
|
||||
|
||||
- 分类:`tools`
|
||||
- 类型:`neo4j`
|
||||
117
apps/docs/content/docs/zh/tools/stt.mdx
Normal file
117
apps/docs/content/docs/zh/tools/stt.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: 语音转文字
|
||||
description: 使用 AI 将语音转换为文字
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="stt"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
使用领先提供商的最先进 AI 模型,将语音转录为文字。Sim 语音转文字 (STT) 工具可以将音频和视频文件转换为准确的文字记录,支持多种语言、时间戳以及可选的翻译功能。
|
||||
|
||||
支持的提供商:
|
||||
|
||||
- **[OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text/overview)**:来自 OpenAI 的先进开源 STT 模型。支持 `whisper-1` 等模型,能够处理多种语言和音频格式。
|
||||
- **[Deepgram](https://deepgram.com/)**:基于深度学习模型的实时和批量 STT API,例如 `nova-3`、`nova-2` 和 `whisper-large`。提供诸如说话人分离、意图识别和行业特定调优等功能。
|
||||
- **[ElevenLabs](https://elevenlabs.io/)**:以高质量语音 AI 闻名,ElevenLabs 提供专注于准确性和自然语言理解的 STT 模型,支持多种语言和方言。
|
||||
|
||||
选择最适合您任务的提供商和模型——无论是快速、生产级转录 (Deepgram),高精度多语言能力 (Whisper),还是高级理解和语言覆盖 (ElevenLabs)。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用说明
|
||||
|
||||
使用领先的 AI 提供商将音频和视频文件转录为文字。支持多种语言、时间戳和说话人分离。
|
||||
|
||||
## 工具
|
||||
|
||||
### `stt_whisper`
|
||||
|
||||
使用 OpenAI Whisper 将音频转录为文字
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | 是 | STT 提供商 \(whisper\) |
|
||||
| `apiKey` | string | 是 | OpenAI API 密钥 |
|
||||
| `model` | string | 否 | 使用的 Whisper 模型 \(默认值:whisper-1\) |
|
||||
| `audioFile` | file | 否 | 要转录的音频或视频文件 |
|
||||
| `audioFileReference` | file | 否 | 来自前一个模块的音频/视频文件引用 |
|
||||
| `audioUrl` | string | 否 | 音频或视频文件的 URL |
|
||||
| `language` | string | 否 | 语言代码 \(例如:"en"、"es"、"fr"\) 或 "auto" 进行自动检测 |
|
||||
| `timestamps` | string | 否 | 时间戳粒度:无、句子或单词 |
|
||||
| `translateToEnglish` | boolean | 否 | 将音频翻译为英语 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完整的转录文本 |
|
||||
| `segments` | array | 带时间戳的片段 |
|
||||
| `language` | string | 检测到的或指定的语言 |
|
||||
| `duration` | number | 音频时长(以秒为单位) |
|
||||
| `confidence` | number | 总体置信度评分 |
|
||||
|
||||
### `stt_deepgram`
|
||||
|
||||
使用 Deepgram 将音频转录为文本
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | 是 | STT 提供商 \(deepgram\) |
|
||||
| `apiKey` | string | 是 | Deepgram API 密钥 |
|
||||
| `model` | string | 否 | 要使用的 Deepgram 模型 \(nova-3, nova-2, whisper-large 等\) |
|
||||
| `audioFile` | file | 否 | 要转录的音频或视频文件 |
|
||||
| `audioFileReference` | file | 否 | 来自前面模块的音频/视频文件引用 |
|
||||
| `audioUrl` | string | 否 | 音频或视频文件的 URL |
|
||||
| `language` | string | 否 | 语言代码 \(例如:"en", "es", "fr"\) 或 "auto" 进行自动检测 |
|
||||
| `timestamps` | string | 否 | 时间戳粒度:无、句子或单词 |
|
||||
| `diarization` | boolean | 否 | 启用说话人分离 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完整的转录文本 |
|
||||
| `segments` | array | 带有说话人标签的时间戳片段 |
|
||||
| `language` | string | 检测到的或指定的语言 |
|
||||
| `duration` | number | 音频时长(以秒为单位) |
|
||||
| `confidence` | number | 总体置信度评分 |
|
||||
|
||||
### `stt_elevenlabs`
|
||||
|
||||
使用 ElevenLabs 将音频转录为文本
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `provider` | string | 是 | STT 提供商 \(elevenlabs\) |
|
||||
| `apiKey` | string | 是 | ElevenLabs API 密钥 |
|
||||
| `model` | string | 否 | 要使用的 ElevenLabs 模型 \(scribe_v1, scribe_v1_experimental\) |
|
||||
| `audioFile` | file | 否 | 要转录的音频或视频文件 |
|
||||
| `audioFileReference` | file | 否 | 来自前面模块的音频/视频文件引用 |
|
||||
| `audioUrl` | string | 否 | 音频或视频文件的 URL |
|
||||
| `language` | string | 否 | 语言代码 \(例如 "en", "es", "fr"\) 或 "auto" 进行自动检测 |
|
||||
| `timestamps` | string | 否 | 时间戳粒度:无、句子或单词 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | string | 完整的转录文本 |
|
||||
| `segments` | array | 带时间戳的片段 |
|
||||
| `language` | string | 检测到或指定的语言 |
|
||||
| `duration` | number | 音频时长(秒) |
|
||||
| `confidence` | number | 总体置信度评分 |
|
||||
|
||||
## 注意
|
||||
|
||||
- 类别: `tools`
|
||||
- 类型: `stt`
|
||||
@@ -1834,7 +1834,7 @@ checksums:
|
||||
content/4: 840a65d7676f756ba632bd7828c53024
|
||||
content/5: 1ee7d3e935ef2c7c535da8c12ee25b56
|
||||
content/6: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/7: 513b14c6b4501f4814c256edc9967883
|
||||
content/7: d064a1edf0d7fdad9ff5974815de9a61
|
||||
content/8: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/9: 5a447127a97cfcaa65d3b989fede92e5
|
||||
content/10: d06a280df6a38b6a382231c8eb602d1d
|
||||
@@ -1854,8 +1854,14 @@ checksums:
|
||||
content/24: 3c13f3daa829266960e367a017cceca5
|
||||
content/25: bcadfc362b69078beee0088e5936c98b
|
||||
content/26: 0c3f7b891315dec911a9c651c2859661
|
||||
content/27: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/28: 93be16cbc63507a9568584e2a30df4b4
|
||||
content/27: 70ca4ee3b2869738e6184f506706ff30
|
||||
content/28: 4414625e16d30b342cfa5684a1f5cef8
|
||||
content/29: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/30: 2a43280485faf5248aaee7b457a2ea58
|
||||
content/31: bcadfc362b69078beee0088e5936c98b
|
||||
content/32: 34d56bbba4787024eabd08affc03d847
|
||||
content/33: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/34: 93be16cbc63507a9568584e2a30df4b4
|
||||
4fea5b273049862aed6ad597bfc35c3f:
|
||||
meta/title: 7e91fe4b762eaa7410fa882788e1504b
|
||||
meta/description: eb391c869e4d86d21e4ee08ffff2a318
|
||||
@@ -1867,25 +1873,25 @@ checksums:
|
||||
content/5: b061763378e5f0aca9a25f819d03961d
|
||||
content/6: 75972cfff5aa2f1d4c24f2a1c867cfb7
|
||||
content/7: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/8: 78ecf15af10a2da337ea4f40d9bdafff
|
||||
content/8: 8579c5fe58782fed019acfd5019c515e
|
||||
content/9: bcadfc362b69078beee0088e5936c98b
|
||||
content/10: 467bff9c1a90c96930d1b05286dd4bf8
|
||||
content/11: ba06fa96a9fe3d308546a32490e5a8d8
|
||||
content/12: 85a73e0898e297bea5a887454c79d79b
|
||||
content/12: 58490686b3358445d2fa89e8a048fb51
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: c76f8eba321f747b91442e808d4acbd8
|
||||
content/14: 9acf9c7ac7b83db796a143abc8f8de0f
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: c02f50f62772f7bc48ec6a6865a440a8
|
||||
content/16: d4ac7483993edc4308e6034d4bd551bd
|
||||
content/17: e13dff194d0bc1cecec833cb9805ceaa
|
||||
content/18: 8813ba0bc9fbf636f3a38e59667df896
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: d71b6bb8e2dd6ce98101aec6a1dd77f2
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: 4b2fa576e3f210461b1b7e72afa3860d
|
||||
content/22: d2f04b0f593a08f7656e7431a6b4e5e5
|
||||
content/23: 9eebc263273839cc24231b56fd90b71d
|
||||
content/24: dc02aaf385a0d428f9e88f652291d3e7
|
||||
content/24: 9acb060c11b48ae498d55aceb053b996
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 3819fef1e4ff4d33f6bd53762b6a230f
|
||||
content/26: 707c54d664fcfc307cea6027721b940b
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: b48618ae66e09c34074972d091ceaef0
|
||||
content/29: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
@@ -5117,7 +5123,7 @@ checksums:
|
||||
content/9: e688b523909d6d6e9966c17892a18c96
|
||||
content/10: e50bd5107ca3410126cf0252b3c47eca
|
||||
content/11: d03d17960348dea95c6df8f46114bd0a
|
||||
content/12: 3850cfbd618a9d1c836fc7086da0f9b4
|
||||
content/12: 80da7e96414b75bb5b910c437bf7894a
|
||||
content/13: 6a7479225be3a7c7a42ba557ece50d03
|
||||
content/14: c64f9cd5168b3e592fe3341cbe1a41fe
|
||||
content/15: 87d6b6280da1c98b1bc291483459c8cf
|
||||
@@ -45194,3 +45200,137 @@ checksums:
|
||||
content/160: fb835329fe9cde08d9ad9730b0305ab3
|
||||
content/161: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/162: 0dc746c73de459c0e96845487cc48997
|
||||
982e788c564de9c81fa9eaabf1a5fa49:
|
||||
meta/title: d7a444976365f685817237ac05fd34f6
|
||||
meta/description: 63ec640159270ed7c005a16400b82415
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: 0cfa7acc2c7996956dadcb6758bad8ec
|
||||
content/2: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/3: afbd33890a86dd4a6fa70f59dfff3e4a
|
||||
content/4: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/5: 297d87df4adc36167d404814ccc57aad
|
||||
content/6: 9f5015e85cfa90efd866e335b60ed088
|
||||
content/7: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/8: 013afc1516faf50d10da5356e347991e
|
||||
content/9: bcadfc362b69078beee0088e5936c98b
|
||||
content/10: 2533ecff0ae49cd92d9fac877df5d7c5
|
||||
content/11: b9cb62986f654ea5f2d26da59a5afcb1
|
||||
content/12: 8302cb799a95c69b688c3cc60c8ca547
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 4fec31a79bd95667b6ff64056456c675
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: 0311d0c811d95a32fe64ada5cb6785a2
|
||||
content/17: 2c73fcb5aaeb831e8445939939391d4a
|
||||
content/18: 18934c5bf0a58751121da8d6ceff03c5
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: 31cc8071914e81be7a2c24e9803ff9c1
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: 8872c76c088384089a167071996e2bfe
|
||||
content/23: bbfe9f1513389634ad99534fc255adcd
|
||||
content/24: 2625b6a063ef4ac36ea91293d46169e7
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 463d2d790179a57459e3baebc9d7b80b
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: b8c51b5e356e5f6887506cc119e1316c
|
||||
content/29: 742f6f06d608746b75f450c9fc6c8aec
|
||||
content/30: ac725c569e0800e74a78c92f2162a558
|
||||
content/31: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/32: 45056fdafa5f66c3fb6e65ff566cef55
|
||||
content/33: bcadfc362b69078beee0088e5936c98b
|
||||
content/34: 7186145dcc99e2aa467ea3829a144ffa
|
||||
content/35: 6e31697819e7e2872512d49d05879bf4
|
||||
content/36: 9aa1ae985022bee500728fb2cc8540e6
|
||||
content/37: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/38: b26edc7400b077817fe1cf652badb9b5
|
||||
content/39: bcadfc362b69078beee0088e5936c98b
|
||||
content/40: 738b71be44f845e51c37741121e7719c
|
||||
content/41: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/42: 1585ad3bfe57b75b3f00c9bbd534b025
|
||||
c3023cb2cf5ac0f87875390c70d177e3:
|
||||
meta/title: c069a454d8bc35905d244eafd5c79c2f
|
||||
meta/description: 3ce073a26805cb234451599729cb72ac
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: 3d83b1ab93a161fe14c7a95fdf384aef
|
||||
content/2: 40ed265403d3fd3fa18fc3b98b7578c1
|
||||
content/3: af9879585e299128fc4bf31f9b5dd26a
|
||||
content/4: 674d6480e3f83ec48de4bffb575fc50c
|
||||
content/5: 7e187025537c8856f1b7b0e9dfd46659
|
||||
content/6: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/7: 8671dfc4df37a8bc2c6fbe0bece84ab4
|
||||
content/8: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/9: 0d6d2999bde4b457c6ab5edd7fb339e6
|
||||
content/10: c1b1e2b091366325fd2a17c38696ccd4
|
||||
content/11: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/12: 4563919e59073792975160200d8a4266
|
||||
content/13: bcadfc362b69078beee0088e5936c98b
|
||||
content/14: 85d884f66535f05fc5a50ef18f8dbd93
|
||||
content/15: 5428e789a73f56957d6132981e439cda
|
||||
content/16: 695984df3b2d9e3ec48e6a3b763b97ce
|
||||
content/17: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/18: 09aa4a088e0f4fa05c3f8a9a1fc12ee6
|
||||
content/19: bcadfc362b69078beee0088e5936c98b
|
||||
content/20: 874a488af0b8a6502cdefe09186b6e04
|
||||
content/21: 674960ea439e26adab6a145ca6a849be
|
||||
content/22: 781970b8ad26fab93806885d9ec2de6f
|
||||
content/23: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/24: 7cf4b4ca74a58ec0547cdb4229f0a986
|
||||
content/25: bcadfc362b69078beee0088e5936c98b
|
||||
content/26: 5ba989b2c70cdb0882ab51c29efad4a8
|
||||
content/27: ca448961a9a911d0e119dc90ca0bf79a
|
||||
content/28: 113eaed5c9ad54d4b84d9ea2b15289d0
|
||||
content/29: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/30: 1090a09c040aa3407b26c05fe8e06a88
|
||||
content/31: bcadfc362b69078beee0088e5936c98b
|
||||
content/32: d9f59b682f82dbec12ff59159edf7570
|
||||
content/33: efc3e3d18ff03e50816783bca3cb327b
|
||||
content/34: c0210d0639be07ce7bc9b006bf7a2e7d
|
||||
content/35: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/36: 4e380133281f2e37b365762684f6878a
|
||||
content/37: bcadfc362b69078beee0088e5936c98b
|
||||
content/38: 597f0113a86f8510a95ab2189bc38316
|
||||
content/39: 28d9a2b0d1b92fb4091a9e695b1668cd
|
||||
content/40: 90fb1f55f511cb4b33fbebda2ede81a9
|
||||
content/41: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/42: 21594ec9e38271cd8479b363c4533a31
|
||||
content/43: bcadfc362b69078beee0088e5936c98b
|
||||
content/44: 7d6ad1d154773b658dca1e75c90bbca1
|
||||
content/45: 0d7c70a5ca931d324189e092d33285dd
|
||||
content/46: 0bb79aa11a04fdc63fd27a25034042bc
|
||||
content/47: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/48: 2fe3a0646c2c342e35e17cbf3acbca33
|
||||
content/49: bcadfc362b69078beee0088e5936c98b
|
||||
content/50: 6f27a3ae833363f1cd6df298979187f5
|
||||
content/51: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/52: b80a888741afcf29ac33c9cadb9f3502
|
||||
d90931017b46914e0c322d9814716544:
|
||||
meta/title: 1f6052d8c66a9656710488fc9a7ea14f
|
||||
meta/description: b47aabe3d468796da9356ca25b5aac0e
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: e2d2b01f5510e19c8ff30bd108d739c9
|
||||
content/2: 1bde652151a4ff8bf0b3c9c782e4ac11
|
||||
content/3: 89bc80328bd72ab149ec8b6845dece8b
|
||||
content/4: 8158ba67c513f34de8932258e57b30f9
|
||||
content/5: 90c4eca7b7524047e3ef6bc1237ad067
|
||||
content/6: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/7: 45fdd3ceca2128a340cafb7faba01278
|
||||
content/8: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/9: 40d2668585c0b17ab8a0e6f7c721e434
|
||||
content/10: 021ca9463ef797c41485cce42b6f7709
|
||||
content/11: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/12: c94fdb65974bc4c4a656f98e31fff0c6
|
||||
content/13: bcadfc362b69078beee0088e5936c98b
|
||||
content/14: a47b7c6ecd9d4f9623faee0f642cd09b
|
||||
content/15: c054a98a01f066a22a0b76af03bba7ae
|
||||
content/16: 9c04c67366eca1a253947ceb20352edf
|
||||
content/17: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/18: aa0bfae1dbcf3a0721aca4cb156a5bcf
|
||||
content/19: bcadfc362b69078beee0088e5936c98b
|
||||
content/20: f9266172e9fb5ef06a83082fa0867e22
|
||||
content/21: dbbd82e674306d7d291392ec2c9230e5
|
||||
content/22: bc25f5ab4531ebc89e325ef174afdb8e
|
||||
content/23: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/24: 76a9ae65d0667171ce08fa8d64afd2a3
|
||||
content/25: bcadfc362b69078beee0088e5936c98b
|
||||
content/26: a47b7c6ecd9d4f9623faee0f642cd09b
|
||||
content/27: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/28: ed565ff5d3337b3e57aaec982d6775c1
|
||||
|
||||
@@ -563,6 +563,32 @@ logger.error('Operation failed', { error })
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Icons from '@/components/icons'
|
||||
import { inter } from '@/app/fonts/inter/inter'
|
||||
|
||||
// AI models and providers
|
||||
const modelProviderIcons = [
|
||||
{ icon: Icons.OpenAIIcon, label: 'OpenAI' },
|
||||
{ icon: Icons.AnthropicIcon, label: 'Anthropic' },
|
||||
@@ -16,7 +15,6 @@ const modelProviderIcons = [
|
||||
{ icon: Icons.ElevenLabsIcon, label: 'ElevenLabs' },
|
||||
]
|
||||
|
||||
// Communication and productivity tools
|
||||
const communicationIcons = [
|
||||
{ icon: Icons.SlackIcon, label: 'Slack' },
|
||||
{ icon: Icons.GmailIcon, label: 'Gmail' },
|
||||
@@ -28,6 +26,7 @@ const communicationIcons = [
|
||||
{ icon: Icons.ConfluenceIcon, label: 'Confluence' },
|
||||
{ icon: Icons.TelegramIcon, label: 'Telegram' },
|
||||
{ icon: Icons.GoogleCalendarIcon, label: 'Google Calendar' },
|
||||
{ icon: Icons.CalendlyIcon, label: 'Calendly' },
|
||||
{ icon: Icons.GoogleDocsIcon, label: 'Google Docs' },
|
||||
{ icon: Icons.BrowserUseIcon, label: 'BrowserUse' },
|
||||
{ icon: Icons.TypeformIcon, label: 'Typeform' },
|
||||
@@ -37,7 +36,6 @@ const communicationIcons = [
|
||||
{ icon: Icons.AirtableIcon, label: 'Airtable' },
|
||||
]
|
||||
|
||||
// Data, storage and search services
|
||||
const dataStorageIcons = [
|
||||
{ icon: Icons.PineconeIcon, label: 'Pinecone' },
|
||||
{ icon: Icons.SupabaseIcon, label: 'Supabase' },
|
||||
|
||||
@@ -20,7 +20,7 @@ interface NavProps {
|
||||
}
|
||||
|
||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||
const [githubStars, setGithubStars] = useState('18k')
|
||||
const [githubStars, setGithubStars] = useState('18.5k')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
@@ -61,6 +61,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
width={450}
|
||||
height={360}
|
||||
className='h-auto w-full'
|
||||
sizes='(max-width: 768px) 100vw, 450px'
|
||||
priority
|
||||
itemProp='image'
|
||||
/>
|
||||
@@ -131,7 +132,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
{related.length > 0 && (
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
|
||||
<h2 className='mb-4 font-medium text-[24px]'>Related posts</h2>
|
||||
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
|
||||
{related.map((p) => (
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-gray-200'>
|
||||
@@ -141,6 +142,8 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
width={600}
|
||||
height={315}
|
||||
className='h-[160px] w-full object-cover'
|
||||
sizes='(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
loading='lazy'
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default async function StudioIndex({
|
||||
</div> */}
|
||||
|
||||
{/* Grid layout for consistent rows */}
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3'>
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{posts.map((p, i) => {
|
||||
return (
|
||||
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
|
||||
@@ -74,6 +74,8 @@ export default async function StudioIndex({
|
||||
width={800}
|
||||
height={450}
|
||||
className='h-48 w-full object-cover'
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
loading='lazy'
|
||||
/>
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<div className='mb-2 text-gray-600 text-xs'>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { validateMicrosoftGraphId } from '@/lib/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -15,15 +12,10 @@ const logger = createLogger('MicrosoftFileAPI')
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const fileId = searchParams.get('fileId')
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
|
||||
if (!credentialId || !fileId) {
|
||||
return NextResponse.json({ error: 'Credential ID and File ID are required' }, { status: 400 })
|
||||
@@ -35,19 +27,27 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: fileIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId,
|
||||
workflowId,
|
||||
requireWorkflowIdForInternal: false,
|
||||
})
|
||||
|
||||
if (!credentials.length) {
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
const status = authz.error === 'Credential not found' ? 404 : 403
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
if (credential.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,46 +15,39 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get the credential ID from the query params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const query = searchParams.get('query') || ''
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credential ID`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get the credential from the database
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
const authz = await authorizeCredentialUse(request, {
|
||||
credentialId,
|
||||
workflowId,
|
||||
requireWorkflowIdForInternal: false,
|
||||
})
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
const status = authz.error === 'Credential not found' ? 404 : 403
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
|
||||
}
|
||||
|
||||
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
|
||||
if (!credential) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const credential = credentials[0]
|
||||
|
||||
// Check if the credential belongs to the user
|
||||
if (credential.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
|
||||
credentialUserId: credential.userId,
|
||||
requestUserId: session.user.id,
|
||||
})
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed using the utility function
|
||||
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render } from '@react-email/components'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import CareersConfirmationEmail from '@/components/emails/careers-confirmation-email'
|
||||
import CareersSubmissionEmail from '@/components/emails/careers-submission-email'
|
||||
import CareersConfirmationEmail from '@/components/emails/careers/careers-confirmation-email'
|
||||
import CareersSubmissionEmail from '@/components/emails/careers/careers-submission-email'
|
||||
import { sendEmail } from '@/lib/email/mailer'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from '@sim/db'
|
||||
import { webhook as webhookTable, workflow as workflowTable } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -35,7 +35,15 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
.from(webhookTable)
|
||||
.innerJoin(workflowTable, eq(webhookTable.workflowId, workflowTable.id))
|
||||
.where(and(eq(webhookTable.isActive, true), eq(webhookTable.provider, 'microsoftteams')))
|
||||
.where(
|
||||
and(
|
||||
eq(webhookTable.isActive, true),
|
||||
or(
|
||||
eq(webhookTable.provider, 'microsoft-teams'),
|
||||
eq(webhookTable.provider, 'microsoftteams')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
`Found ${webhooksWithWorkflows.length} active Teams webhooks, checking for expiring subscriptions`
|
||||
|
||||
@@ -13,21 +13,37 @@ import {
|
||||
} from '@/app/api/files/utils'
|
||||
|
||||
const ALLOWED_EXTENSIONS = new Set([
|
||||
// Documents
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'txt',
|
||||
'md',
|
||||
'png',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'gif',
|
||||
'csv',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'json',
|
||||
'yaml',
|
||||
'yml',
|
||||
// Images
|
||||
'png',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'gif',
|
||||
// Audio
|
||||
'mp3',
|
||||
'm4a',
|
||||
'wav',
|
||||
'webm',
|
||||
'ogg',
|
||||
'flac',
|
||||
'aac',
|
||||
'opus',
|
||||
// Video
|
||||
'mp4',
|
||||
'mov',
|
||||
'avi',
|
||||
'mkv',
|
||||
])
|
||||
|
||||
function validateFileExtension(filename: string): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { memory } from '@sim/db/schema'
|
||||
import { memory, workflowBlocks } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
@@ -8,6 +8,43 @@ import { generateRequestId } from '@/lib/utils'
|
||||
|
||||
const logger = createLogger('MemoryByIdAPI')
|
||||
|
||||
/**
|
||||
* Parse memory key into conversationId and blockId
|
||||
* Key format: conversationId:blockId
|
||||
*/
|
||||
function parseMemoryKey(key: string): { conversationId: string; blockId: string } | null {
|
||||
const parts = key.split(':')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
conversationId: parts[0],
|
||||
blockId: parts[1],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup block name from block ID
|
||||
*/
|
||||
async function getBlockName(blockId: string, workflowId: string): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await db
|
||||
.select({ name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result[0].name
|
||||
} catch (error) {
|
||||
logger.error('Error looking up block name', { error, blockId, workflowId })
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const memoryQuerySchema = z.object({
|
||||
workflowId: z.string().uuid('Invalid workflow ID format'),
|
||||
})
|
||||
@@ -41,7 +78,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory get request for ID: ${id}`)
|
||||
|
||||
// Get workflowId from query parameter (required)
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
@@ -65,7 +101,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
|
||||
// Query the database for the memory
|
||||
const memories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
@@ -86,13 +121,36 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
)
|
||||
}
|
||||
|
||||
const mem = memories[0]
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = (await getBlockName(blockId, validatedWorkflowId)) || 'unknown'
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory retrieved successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: memories[0],
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
@@ -122,7 +180,6 @@ export async function DELETE(
|
||||
try {
|
||||
logger.info(`[${requestId}] Processing memory delete request for ID: ${id}`)
|
||||
|
||||
// Get workflowId from query parameter (required)
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
|
||||
@@ -146,7 +203,6 @@ export async function DELETE(
|
||||
|
||||
const { workflowId: validatedWorkflowId } = validation.data
|
||||
|
||||
// Verify memory exists before attempting to delete
|
||||
const existingMemory = await db
|
||||
.select({ id: memory.id })
|
||||
.from(memory)
|
||||
@@ -166,7 +222,6 @@ export async function DELETE(
|
||||
)
|
||||
}
|
||||
|
||||
// Hard delete the memory
|
||||
await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
@@ -241,7 +296,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
)
|
||||
}
|
||||
|
||||
// Verify memory exists before attempting to update
|
||||
const existingMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
@@ -261,47 +315,68 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
)
|
||||
}
|
||||
|
||||
const existingMemory = existingMemories[0]
|
||||
|
||||
// Additional validation for agent memory type
|
||||
if (existingMemory.type === 'agent') {
|
||||
const agentValidation = agentMemoryDataSchema.safeParse(validatedData)
|
||||
if (!agentValidation.success) {
|
||||
const errorMessage = agentValidation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Agent memory validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Invalid agent memory data: ${errorMessage}`,
|
||||
},
|
||||
const agentValidation = agentMemoryDataSchema.safeParse(validatedData)
|
||||
if (!agentValidation.success) {
|
||||
const errorMessage = agentValidation.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ')
|
||||
logger.warn(`[${requestId}] Agent memory validation error: ${errorMessage}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Invalid agent memory data: ${errorMessage}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Update the memory with new data
|
||||
const now = new Date()
|
||||
await db
|
||||
.delete(memory)
|
||||
.update(memory)
|
||||
.set({
|
||||
data: validatedData,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
|
||||
// Fetch the updated memory
|
||||
const updatedMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, id), eq(memory.workflowId, validatedWorkflowId)))
|
||||
.limit(1)
|
||||
|
||||
const mem = updatedMemories[0]
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = (await getBlockName(blockId, validatedWorkflowId)) || 'unknown'
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Memory updated successfully: ${id} for workflow: ${validatedWorkflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: updatedMemories[0],
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import { db } from '@sim/db'
|
||||
import { memory } from '@sim/db/schema'
|
||||
import { and, eq, isNull, like } from 'drizzle-orm'
|
||||
import { memory, workflowBlocks } from '@sim/db/schema'
|
||||
import { and, eq, inArray, isNull, like } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('MemoryAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* Parse memory key into conversationId and blockId
|
||||
* Key format: conversationId:blockId
|
||||
* @param key The memory key to parse
|
||||
* @returns Object with conversationId and blockId, or null if invalid
|
||||
*/
|
||||
function parseMemoryKey(key: string): { conversationId: string; blockId: string } | null {
|
||||
const parts = key.split(':')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
conversationId: parts[0],
|
||||
blockId: parts[1],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET handler for searching and retrieving memories
|
||||
* Supports query parameters:
|
||||
@@ -22,16 +41,28 @@ export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request)
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory search request`)
|
||||
|
||||
// Extract workflowId from query parameters
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const searchQuery = url.searchParams.get('query')
|
||||
const type = url.searchParams.get('type')
|
||||
const blockNameFilter = url.searchParams.get('blockName')
|
||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||
|
||||
// Require workflowId for security
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
return NextResponse.json(
|
||||
@@ -45,38 +76,148 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Build query conditions
|
||||
const conditions = []
|
||||
|
||||
// Only include non-deleted memories
|
||||
conditions.push(isNull(memory.deletedAt))
|
||||
|
||||
// Filter by workflow ID (required)
|
||||
conditions.push(eq(memory.workflowId, workflowId))
|
||||
|
||||
// Add type filter if provided
|
||||
if (type) {
|
||||
conditions.push(eq(memory.type, type))
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
if (!isOwner && !workspacePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied access to workflow ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) accessing memories for workflow ${workflowId}`
|
||||
)
|
||||
|
||||
const conditions = []
|
||||
|
||||
conditions.push(isNull(memory.deletedAt))
|
||||
|
||||
conditions.push(eq(memory.workflowId, workflowId))
|
||||
|
||||
let blockIdsToFilter: string[] | null = null
|
||||
if (blockNameFilter) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(
|
||||
and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockNameFilter))
|
||||
)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
logger.info(
|
||||
`[${requestId}] No blocks found with name "${blockNameFilter}" for workflow: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { memories: [] },
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
blockIdsToFilter = blocks.map((b) => b.id)
|
||||
}
|
||||
|
||||
// Add search query if provided (leverages index on key field)
|
||||
if (searchQuery) {
|
||||
conditions.push(like(memory.key, `%${searchQuery}%`))
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
const memories = await db
|
||||
const rawMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(...conditions))
|
||||
.orderBy(memory.createdAt)
|
||||
.limit(limit)
|
||||
|
||||
logger.info(`[${requestId}] Found ${memories.length} memories for workflow: ${workflowId}`)
|
||||
const filteredMemories = blockIdsToFilter
|
||||
? rawMemories.filter((mem) => {
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
return parsed && blockIdsToFilter.includes(parsed.blockId)
|
||||
})
|
||||
: rawMemories
|
||||
|
||||
const blockIds = new Set<string>()
|
||||
const parsedKeys = new Map<string, { conversationId: string; blockId: string }>()
|
||||
|
||||
for (const mem of filteredMemories) {
|
||||
const parsed = parseMemoryKey(mem.key)
|
||||
if (parsed) {
|
||||
blockIds.add(parsed.blockId)
|
||||
parsedKeys.set(mem.key, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const blockNameMap = new Map<string, string>()
|
||||
if (blockIds.size > 0) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id, name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowBlocks.workflowId, workflowId),
|
||||
inArray(workflowBlocks.id, Array.from(blockIds))
|
||||
)
|
||||
)
|
||||
|
||||
for (const block of blocks) {
|
||||
blockNameMap.set(block.id, block.name)
|
||||
}
|
||||
}
|
||||
|
||||
const enrichedMemories = filteredMemories.map((mem) => {
|
||||
const parsed = parsedKeys.get(mem.key)
|
||||
|
||||
if (!parsed) {
|
||||
return {
|
||||
conversationId: mem.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: mem.data,
|
||||
}
|
||||
}
|
||||
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = blockNameMap.get(blockId) || 'unknown'
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: mem.data,
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Found ${enrichedMemories.length} memories for workflow: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { memories },
|
||||
data: { memories: enrichedMemories },
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
@@ -105,13 +246,25 @@ export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request)
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory creation request`)
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json()
|
||||
const { key, type, data, workflowId } = body
|
||||
const { key, data, workflowId } = body
|
||||
|
||||
// Validate required fields
|
||||
if (!key) {
|
||||
logger.warn(`[${requestId}] Missing required field: key`)
|
||||
return NextResponse.json(
|
||||
@@ -125,19 +278,6 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
if (!type || type !== 'agent') {
|
||||
logger.warn(`[${requestId}] Invalid memory type: ${type}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Memory type must be "agent"',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
logger.warn(`[${requestId}] Missing required field: data`)
|
||||
return NextResponse.json(
|
||||
@@ -164,28 +304,67 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Additional validation for agent type
|
||||
if (type === 'agent') {
|
||||
if (!data.role || !data.content) {
|
||||
logger.warn(`[${requestId}] Missing agent memory fields`)
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
const hasWritePermission =
|
||||
isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
|
||||
|
||||
if (!hasWritePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied write access to workflow ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Write access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) creating memory for workflow ${workflowId}`
|
||||
)
|
||||
|
||||
const dataToValidate = Array.isArray(data) ? data : [data]
|
||||
|
||||
for (const msg of dataToValidate) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.role || !msg.content) {
|
||||
logger.warn(`[${requestId}] Missing required message fields`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Agent memory requires role and content',
|
||||
message: 'Memory requires messages with role and content',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['user', 'assistant', 'system'].includes(data.role)) {
|
||||
logger.warn(`[${requestId}] Invalid agent role: ${data.role}`)
|
||||
if (!['user', 'assistant', 'system'].includes(msg.role)) {
|
||||
logger.warn(`[${requestId}] Invalid message role: ${msg.role}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Agent role must be user, assistant, or system',
|
||||
message: 'Message role must be user, assistant, or system',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
@@ -193,77 +372,34 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if memory with the same key already exists for this workflow
|
||||
const existingMemory = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId), isNull(memory.deletedAt)))
|
||||
.limit(1)
|
||||
const initialData = Array.isArray(data) ? data : [data]
|
||||
const now = new Date()
|
||||
const id = `mem_${crypto.randomUUID().replace(/-/g, '')}`
|
||||
|
||||
let statusCode = 201 // Default status code for new memory
|
||||
const { sql } = await import('drizzle-orm')
|
||||
|
||||
if (existingMemory.length > 0) {
|
||||
logger.info(`[${requestId}] Memory with key ${key} exists, checking if we can append`)
|
||||
|
||||
// Check if types match
|
||||
if (existingMemory[0].type !== type) {
|
||||
logger.warn(
|
||||
`[${requestId}] Memory type mismatch: existing=${existingMemory[0].type}, new=${type}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: `Cannot append memory of type '${type}' to existing memory of type '${existingMemory[0].type}'`,
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Handle appending for agent type
|
||||
let updatedData
|
||||
|
||||
// For agent type
|
||||
const newMessage = data
|
||||
const existingData = existingMemory[0].data
|
||||
|
||||
// If existing data is an array, append to it
|
||||
if (Array.isArray(existingData)) {
|
||||
updatedData = [...existingData, newMessage]
|
||||
}
|
||||
// If existing data is a single message object, convert to array
|
||||
else {
|
||||
updatedData = [existingData, newMessage]
|
||||
}
|
||||
|
||||
// Update the existing memory with appended data
|
||||
await db
|
||||
.update(memory)
|
||||
.set({
|
||||
data: updatedData,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
|
||||
|
||||
statusCode = 200 // Status code for updated memory
|
||||
} else {
|
||||
// Insert the new memory
|
||||
const newMemory = {
|
||||
id: `mem_${crypto.randomUUID().replace(/-/g, '')}`,
|
||||
await db
|
||||
.insert(memory)
|
||||
.values({
|
||||
id,
|
||||
workflowId,
|
||||
key,
|
||||
type,
|
||||
data: Array.isArray(data) ? data : [data],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
data: initialData,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [memory.workflowId, memory.key],
|
||||
set: {
|
||||
data: sql`${memory.data} || ${JSON.stringify(initialData)}::jsonb`,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
|
||||
await db.insert(memory).values(newMemory)
|
||||
logger.info(`[${requestId}] Memory created successfully: ${key} for workflow: ${workflowId}`)
|
||||
}
|
||||
logger.info(
|
||||
`[${requestId}] Memory operation successful (atomic): ${key} for workflow: ${workflowId}`
|
||||
)
|
||||
|
||||
// Fetch all memories with the same key for this workflow to return the complete list
|
||||
const allMemories = await db
|
||||
.select()
|
||||
.from(memory)
|
||||
@@ -271,7 +407,6 @@ export async function POST(request: NextRequest) {
|
||||
.orderBy(memory.createdAt)
|
||||
|
||||
if (allMemories.length === 0) {
|
||||
// This shouldn't happen but handle it just in case
|
||||
logger.warn(`[${requestId}] No memories found after creating/updating memory: ${key}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -284,19 +419,44 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get the memory object to return
|
||||
const memoryRecord = allMemories[0]
|
||||
const parsed = parseMemoryKey(memoryRecord.key)
|
||||
|
||||
let enrichedMemory
|
||||
if (!parsed) {
|
||||
enrichedMemory = {
|
||||
conversationId: memoryRecord.key,
|
||||
blockId: 'unknown',
|
||||
blockName: 'unknown',
|
||||
data: memoryRecord.data,
|
||||
}
|
||||
} else {
|
||||
const { conversationId, blockId } = parsed
|
||||
const blockName = await (async () => {
|
||||
const blocks = await db
|
||||
.select({ name: workflowBlocks.name })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
|
||||
.limit(1)
|
||||
return blocks.length > 0 ? blocks[0].name : 'unknown'
|
||||
})()
|
||||
|
||||
enrichedMemory = {
|
||||
conversationId,
|
||||
blockId,
|
||||
blockName,
|
||||
data: memoryRecord.data,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Memory operation successful: ${key} for workflow: ${workflowId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: memoryRecord,
|
||||
data: enrichedMemory,
|
||||
},
|
||||
{ status: statusCode }
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
// Handle unique constraint violation
|
||||
if (error.code === '23505') {
|
||||
logger.warn(`[${requestId}] Duplicate key violation`)
|
||||
return NextResponse.json(
|
||||
@@ -321,3 +481,215 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE handler for pattern-based memory deletion
|
||||
* Supports query parameters:
|
||||
* - workflowId: Required
|
||||
* - conversationId: Optional - delete all memories for this conversation
|
||||
* - blockId: Optional - delete all memories for this block
|
||||
* - blockName: Optional - delete all memories for blocks with this name
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request)
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: authResult.error || 'Authentication required',
|
||||
},
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Processing memory deletion request`)
|
||||
|
||||
const url = new URL(request.url)
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const conversationId = url.searchParams.get('conversationId')
|
||||
const blockId = url.searchParams.get('blockId')
|
||||
const blockName = url.searchParams.get('blockName')
|
||||
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required parameter: workflowId`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'workflowId parameter is required',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!conversationId && !blockId && !blockName) {
|
||||
logger.warn(`[${requestId}] No filter parameters provided`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'At least one of conversationId, blockId, or blockName must be provided',
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, authResult.userId)
|
||||
if (!accessContext) {
|
||||
logger.warn(`[${requestId}] Workflow ${workflowId} not found for user ${authResult.userId}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const { workspacePermission, isOwner } = accessContext
|
||||
|
||||
const hasWritePermission =
|
||||
isOwner || workspacePermission === 'write' || workspacePermission === 'admin'
|
||||
|
||||
if (!hasWritePermission) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${authResult.userId} denied delete access to workflow ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Write access denied to this workflow',
|
||||
},
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] User ${authResult.userId} (${authResult.authType}) deleting memories for workflow ${workflowId}`
|
||||
)
|
||||
|
||||
let deletedCount = 0
|
||||
|
||||
if (conversationId && blockId) {
|
||||
const key = `${conversationId}:${blockId}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (conversationId && blockName) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockName)))
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message: `No blocks found with name "${blockName}"`,
|
||||
deletedCount: 0,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const key = `${conversationId}:${block.id}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(eq(memory.key, key), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount += result.length
|
||||
}
|
||||
} else if (conversationId) {
|
||||
const pattern = `${conversationId}:%`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (blockId) {
|
||||
const pattern = `%:${blockId}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount = result.length
|
||||
} else if (blockName) {
|
||||
const blocks = await db
|
||||
.select({ id: workflowBlocks.id })
|
||||
.from(workflowBlocks)
|
||||
.where(and(eq(workflowBlocks.workflowId, workflowId), eq(workflowBlocks.name, blockName)))
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message: `No blocks found with name "${blockName}"`,
|
||||
deletedCount: 0,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const pattern = `%:${block.id}`
|
||||
const result = await db
|
||||
.delete(memory)
|
||||
.where(and(like(memory.key, pattern), eq(memory.workflowId, workflowId)))
|
||||
.returning({ id: memory.id })
|
||||
|
||||
deletedCount += result.length
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully deleted ${deletedCount} memories for workflow: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
message:
|
||||
deletedCount > 0
|
||||
? `Successfully deleted ${deletedCount} memories`
|
||||
: 'No memories found matching the criteria',
|
||||
deletedCount,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error deleting memories`, { error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message || 'Failed to delete memories',
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
375
apps/sim/app/api/proxy/stt/route.ts
Normal file
375
apps/sim/app/api/proxy/stt/route.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { TranscriptSegment } from '@/tools/stt/types'
|
||||
|
||||
const logger = createLogger('SttProxyAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 300 // 5 minutes for large files
|
||||
|
||||
interface SttRequestBody {
|
||||
provider: 'whisper' | 'deepgram' | 'elevenlabs'
|
||||
apiKey: string
|
||||
model?: string
|
||||
audioFile?: UserFile | UserFile[]
|
||||
audioFileReference?: UserFile | UserFile[]
|
||||
audioUrl?: string
|
||||
language?: string
|
||||
timestamps?: 'none' | 'sentence' | 'word'
|
||||
diarization?: boolean
|
||||
translateToEnglish?: boolean
|
||||
workspaceId?: string
|
||||
workflowId?: string
|
||||
executionId?: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID()
|
||||
logger.info(`[${requestId}] STT transcription request started`)
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body: SttRequestBody = await request.json()
|
||||
const { provider, apiKey, model, language, timestamps, diarization, translateToEnglish } = body
|
||||
|
||||
if (!provider || !apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: provider and apiKey' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let audioBuffer: Buffer
|
||||
let audioFileName: string
|
||||
let audioMimeType: string
|
||||
|
||||
if (body.audioFile) {
|
||||
const file = Array.isArray(body.audioFile) ? body.audioFile[0] : body.audioFile
|
||||
logger.info(`[${requestId}] Processing uploaded file: ${file.name}`)
|
||||
|
||||
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
audioFileName = file.name
|
||||
audioMimeType = file.type
|
||||
} else if (body.audioFileReference) {
|
||||
const file = Array.isArray(body.audioFileReference)
|
||||
? body.audioFileReference[0]
|
||||
: body.audioFileReference
|
||||
logger.info(`[${requestId}] Processing referenced file: ${file.name}`)
|
||||
|
||||
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
audioFileName = file.name
|
||||
audioMimeType = file.type
|
||||
} else if (body.audioUrl) {
|
||||
logger.info(`[${requestId}] Downloading from URL: ${body.audioUrl}`)
|
||||
|
||||
const response = await fetch(body.audioUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download audio from URL: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
audioBuffer = Buffer.from(arrayBuffer)
|
||||
audioFileName = body.audioUrl.split('/').pop() || 'audio_file'
|
||||
audioMimeType = response.headers.get('content-type') || 'audio/mpeg'
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'No audio source provided. Provide audioFile, audioFileReference, or audioUrl' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (isVideoFile(audioMimeType)) {
|
||||
logger.info(`[${requestId}] Extracting audio from video file`)
|
||||
try {
|
||||
const extracted = await extractAudioFromVideo(audioBuffer, audioMimeType, {
|
||||
outputFormat: 'mp3',
|
||||
sampleRate: 16000,
|
||||
channels: 1,
|
||||
})
|
||||
audioBuffer = extracted.buffer
|
||||
audioMimeType = 'audio/mpeg'
|
||||
audioFileName = audioFileName.replace(/\.[^.]+$/, '.mp3')
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Video extraction failed:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Failed to extract audio from video: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Transcribing with ${provider}, file: ${audioFileName}`)
|
||||
|
||||
let transcript: string
|
||||
let segments: TranscriptSegment[] | undefined
|
||||
let detectedLanguage: string | undefined
|
||||
let duration: number | undefined
|
||||
let confidence: number | undefined
|
||||
|
||||
try {
|
||||
if (provider === 'whisper') {
|
||||
const result = await transcribeWithWhisper(
|
||||
audioBuffer,
|
||||
apiKey,
|
||||
language,
|
||||
timestamps,
|
||||
translateToEnglish,
|
||||
model
|
||||
)
|
||||
transcript = result.transcript
|
||||
segments = result.segments
|
||||
detectedLanguage = result.language
|
||||
duration = result.duration
|
||||
} else if (provider === 'deepgram') {
|
||||
const result = await transcribeWithDeepgram(
|
||||
audioBuffer,
|
||||
apiKey,
|
||||
language,
|
||||
timestamps,
|
||||
diarization,
|
||||
model
|
||||
)
|
||||
transcript = result.transcript
|
||||
segments = result.segments
|
||||
detectedLanguage = result.language
|
||||
duration = result.duration
|
||||
confidence = result.confidence
|
||||
} else if (provider === 'elevenlabs') {
|
||||
const result = await transcribeWithElevenLabs(
|
||||
audioBuffer,
|
||||
apiKey,
|
||||
language,
|
||||
timestamps,
|
||||
model
|
||||
)
|
||||
transcript = result.transcript
|
||||
segments = result.segments
|
||||
detectedLanguage = result.language
|
||||
duration = result.duration
|
||||
} else {
|
||||
return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 400 })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Transcription failed:`, error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Transcription failed'
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Transcription completed successfully`)
|
||||
|
||||
return NextResponse.json({
|
||||
transcript,
|
||||
segments,
|
||||
language: detectedLanguage,
|
||||
duration,
|
||||
confidence,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] STT proxy error:`, error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeWithWhisper(
|
||||
audioBuffer: Buffer,
|
||||
apiKey: string,
|
||||
language?: string,
|
||||
timestamps?: 'none' | 'sentence' | 'word',
|
||||
translate?: boolean,
|
||||
model?: string
|
||||
): Promise<{
|
||||
transcript: string
|
||||
segments?: TranscriptSegment[]
|
||||
language?: string
|
||||
duration?: number
|
||||
}> {
|
||||
const formData = new FormData()
|
||||
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/mpeg' })
|
||||
formData.append('file', blob, 'audio.mp3')
|
||||
formData.append('model', model || 'whisper-1')
|
||||
|
||||
if (language && language !== 'auto') {
|
||||
formData.append('language', language)
|
||||
}
|
||||
|
||||
if (timestamps === 'word') {
|
||||
formData.append('response_format', 'verbose_json')
|
||||
formData.append('timestamp_granularities[]', 'word')
|
||||
} else if (timestamps === 'sentence') {
|
||||
formData.append('response_format', 'verbose_json')
|
||||
formData.append('timestamp_granularities[]', 'segment')
|
||||
}
|
||||
|
||||
const endpoint = translate ? 'translations' : 'transcriptions'
|
||||
const response = await fetch(`https://api.openai.com/v1/audio/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const errorMessage = error.error?.message || error.message || JSON.stringify(error)
|
||||
throw new Error(`Whisper API error: ${errorMessage}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (timestamps === 'none') {
|
||||
return {
|
||||
transcript: data.text,
|
||||
language: data.language,
|
||||
}
|
||||
}
|
||||
const segments: TranscriptSegment[] = (data.segments || data.words || []).map((seg: any) => ({
|
||||
text: seg.text,
|
||||
start: seg.start,
|
||||
end: seg.end,
|
||||
}))
|
||||
|
||||
return {
|
||||
transcript: data.text,
|
||||
segments,
|
||||
language: data.language,
|
||||
duration: data.duration,
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeWithDeepgram(
|
||||
audioBuffer: Buffer,
|
||||
apiKey: string,
|
||||
language?: string,
|
||||
timestamps?: 'none' | 'sentence' | 'word',
|
||||
diarization?: boolean,
|
||||
model?: string
|
||||
): Promise<{
|
||||
transcript: string
|
||||
segments?: TranscriptSegment[]
|
||||
language?: string
|
||||
duration?: number
|
||||
confidence?: number
|
||||
}> {
|
||||
const params = new URLSearchParams({
|
||||
model: model || 'nova-3',
|
||||
smart_format: 'true',
|
||||
punctuate: 'true',
|
||||
})
|
||||
|
||||
if (language && language !== 'auto') {
|
||||
params.append('language', language)
|
||||
}
|
||||
|
||||
if (timestamps !== 'none') {
|
||||
params.append('utterances', 'true')
|
||||
}
|
||||
|
||||
if (diarization) {
|
||||
params.append('diarize', 'true')
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.deepgram.com/v1/listen?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${apiKey}`,
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
body: new Uint8Array(audioBuffer),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const errorMessage = error.err_msg || error.message || JSON.stringify(error)
|
||||
throw new Error(`Deepgram API error: ${errorMessage}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const result = data.results?.channels?.[0]?.alternatives?.[0]
|
||||
|
||||
if (!result) {
|
||||
throw new Error('No transcription result from Deepgram')
|
||||
}
|
||||
|
||||
const transcript = result.transcript
|
||||
const detectedLanguage = data.results?.channels?.[0]?.detected_language
|
||||
const confidence = result.confidence
|
||||
|
||||
let segments: TranscriptSegment[] | undefined
|
||||
if (timestamps !== 'none' && result.words) {
|
||||
segments = result.words.map((word: any) => ({
|
||||
text: word.word,
|
||||
start: word.start,
|
||||
end: word.end,
|
||||
speaker: word.speaker !== undefined ? `Speaker ${word.speaker}` : undefined,
|
||||
confidence: word.confidence,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
transcript,
|
||||
segments,
|
||||
language: detectedLanguage,
|
||||
duration: data.metadata?.duration,
|
||||
confidence,
|
||||
}
|
||||
}
|
||||
|
||||
async function transcribeWithElevenLabs(
|
||||
audioBuffer: Buffer,
|
||||
apiKey: string,
|
||||
language?: string,
|
||||
timestamps?: 'none' | 'sentence' | 'word',
|
||||
model?: string
|
||||
): Promise<{
|
||||
transcript: string
|
||||
segments?: TranscriptSegment[]
|
||||
language?: string
|
||||
duration?: number
|
||||
}> {
|
||||
const formData = new FormData()
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/mpeg' })
|
||||
formData.append('file', blob, 'audio.mp3')
|
||||
formData.append('model_id', model || 'scribe_v1')
|
||||
|
||||
if (language && language !== 'auto') {
|
||||
formData.append('language', language)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.elevenlabs.io/v1/speech-to-text', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': apiKey,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const errorMessage =
|
||||
typeof error.detail === 'string'
|
||||
? error.detail
|
||||
: error.detail?.message || error.message || JSON.stringify(error)
|
||||
throw new Error(`ElevenLabs API error: ${errorMessage}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
transcript: data.text || '',
|
||||
language: data.language,
|
||||
duration: data.duration,
|
||||
}
|
||||
}
|
||||
118
apps/sim/app/api/tools/neo4j/create/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/create/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jCreateAPI')
|
||||
|
||||
const CreateSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = CreateSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j create on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Create executed successfully, created ${summary.counters.nodesCreated} nodes and ${summary.counters.relationshipsCreated} relationships, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Created ${summary.counters.nodesCreated} nodes and ${summary.counters.relationshipsCreated} relationships`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j create failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j create failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
103
apps/sim/app/api/tools/neo4j/delete/route.ts
Normal file
103
apps/sim/app/api/tools/neo4j/delete/route.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { createNeo4jDriver, validateCypherQuery } from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jDeleteAPI')
|
||||
|
||||
const DeleteSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
detach: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j delete on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Delete executed successfully, deleted ${summary.counters.nodesDeleted} nodes and ${summary.counters.relationshipsDeleted} relationships`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Deleted ${summary.counters.nodesDeleted} nodes and ${summary.counters.relationshipsDeleted} relationships`,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j delete failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j delete failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
116
apps/sim/app/api/tools/neo4j/execute/route.ts
Normal file
116
apps/sim/app/api/tools/neo4j/execute/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jExecuteAPI')
|
||||
|
||||
const ExecuteSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ExecuteSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Query executed successfully, returned ${records.length} records`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Query executed successfully, returned ${records.length} records`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j execute failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j execute failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
apps/sim/app/api/tools/neo4j/merge/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/merge/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jMergeAPI')
|
||||
|
||||
const MergeSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = MergeSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j merge on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Merge executed successfully, created ${summary.counters.nodesCreated} nodes, ${summary.counters.relationshipsCreated} relationships, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Merge completed: ${summary.counters.nodesCreated} nodes created, ${summary.counters.relationshipsCreated} relationships created`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j merge failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j merge failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
116
apps/sim/app/api/tools/neo4j/query/route.ts
Normal file
116
apps/sim/app/api/tools/neo4j/query/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jQueryAPI')
|
||||
|
||||
const QuerySchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = QuerySchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Query executed successfully, returned ${records.length} records`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Found ${records.length} records`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j query failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j query failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
apps/sim/app/api/tools/neo4j/update/route.ts
Normal file
118
apps/sim/app/api/tools/neo4j/update/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
convertNeo4jTypesToJSON,
|
||||
createNeo4jDriver,
|
||||
validateCypherQuery,
|
||||
} from '@/app/api/tools/neo4j/utils'
|
||||
|
||||
const logger = createLogger('Neo4jUpdateAPI')
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
host: z.string().min(1, 'Host is required'),
|
||||
port: z.coerce.number().int().positive('Port must be a positive integer'),
|
||||
database: z.string().min(1, 'Database name is required'),
|
||||
username: z.string().min(1, 'Username is required'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
encryption: z.enum(['enabled', 'disabled']).default('disabled'),
|
||||
cypherQuery: z.string().min(1, 'Cypher query is required'),
|
||||
parameters: z.record(z.unknown()).nullable().optional().default({}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
let driver = null
|
||||
let session = null
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = UpdateSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing Neo4j update on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const validation = validateCypherQuery(params.cypherQuery)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Cypher query validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Query validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
driver = await createNeo4jDriver({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
encryption: params.encryption,
|
||||
})
|
||||
|
||||
session = driver.session({ database: params.database })
|
||||
|
||||
const result = await session.run(params.cypherQuery, params.parameters)
|
||||
|
||||
const records = result.records.map((record) => {
|
||||
const obj: Record<string, unknown> = {}
|
||||
record.keys.forEach((key) => {
|
||||
if (typeof key === 'string') {
|
||||
obj[key] = convertNeo4jTypesToJSON(record.get(key))
|
||||
}
|
||||
})
|
||||
return obj
|
||||
})
|
||||
|
||||
const summary = {
|
||||
resultAvailableAfter: result.summary.resultAvailableAfter.toNumber(),
|
||||
resultConsumedAfter: result.summary.resultConsumedAfter.toNumber(),
|
||||
counters: {
|
||||
nodesCreated: result.summary.counters.updates().nodesCreated,
|
||||
nodesDeleted: result.summary.counters.updates().nodesDeleted,
|
||||
relationshipsCreated: result.summary.counters.updates().relationshipsCreated,
|
||||
relationshipsDeleted: result.summary.counters.updates().relationshipsDeleted,
|
||||
propertiesSet: result.summary.counters.updates().propertiesSet,
|
||||
labelsAdded: result.summary.counters.updates().labelsAdded,
|
||||
labelsRemoved: result.summary.counters.updates().labelsRemoved,
|
||||
indexesAdded: result.summary.counters.updates().indexesAdded,
|
||||
indexesRemoved: result.summary.counters.updates().indexesRemoved,
|
||||
constraintsAdded: result.summary.counters.updates().constraintsAdded,
|
||||
constraintsRemoved: result.summary.counters.updates().constraintsRemoved,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Update executed successfully, ${summary.counters.propertiesSet} properties set, returned ${records.length} records`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Updated ${summary.counters.propertiesSet} properties`,
|
||||
records,
|
||||
recordCount: records.length,
|
||||
summary,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Neo4j update failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `Neo4j update failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.close()
|
||||
}
|
||||
if (driver) {
|
||||
await driver.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
164
apps/sim/app/api/tools/neo4j/utils.ts
Normal file
164
apps/sim/app/api/tools/neo4j/utils.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import neo4j from 'neo4j-driver'
|
||||
import type { Neo4jConnectionConfig } from '@/tools/neo4j/types'
|
||||
|
||||
export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
|
||||
const isAuraHost =
|
||||
config.host === 'databases.neo4j.io' || config.host.endsWith('.databases.neo4j.io')
|
||||
|
||||
let protocol: string
|
||||
if (isAuraHost) {
|
||||
protocol = 'neo4j+s'
|
||||
} else {
|
||||
protocol = config.encryption === 'enabled' ? 'bolt+s' : 'bolt'
|
||||
}
|
||||
|
||||
const uri = `${protocol}://${config.host}:${config.port}`
|
||||
|
||||
const driverConfig: any = {
|
||||
maxConnectionPoolSize: 1,
|
||||
connectionTimeout: 10000,
|
||||
}
|
||||
|
||||
if (!protocol.endsWith('+s')) {
|
||||
driverConfig.encrypted = config.encryption === 'enabled' ? 'ENCRYPTION_ON' : 'ENCRYPTION_OFF'
|
||||
}
|
||||
|
||||
const driver = neo4j.driver(uri, neo4j.auth.basic(config.username, config.password), driverConfig)
|
||||
|
||||
await driver.verifyConnectivity()
|
||||
|
||||
return driver
|
||||
}
|
||||
|
||||
export function validateCypherQuery(
|
||||
query: string,
|
||||
allowDangerousOps = false
|
||||
): { isValid: boolean; error?: string } {
|
||||
if (!query || typeof query !== 'string') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Query must be a non-empty string',
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowDangerousOps) {
|
||||
const dangerousPatterns = [
|
||||
/DROP\s+DATABASE/i,
|
||||
/DROP\s+CONSTRAINT/i,
|
||||
/DROP\s+INDEX/i,
|
||||
/CREATE\s+DATABASE/i,
|
||||
/CREATE\s+CONSTRAINT/i,
|
||||
/CREATE\s+INDEX/i,
|
||||
/CALL\s+dbms\./i,
|
||||
/CALL\s+db\./i,
|
||||
/LOAD\s+CSV/i,
|
||||
/apoc\.cypher\.run/i,
|
||||
/apoc\.load/i,
|
||||
/apoc\.periodic/i,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(query)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
'Query contains potentially dangerous operations (schema changes, system procedures, or external data loading)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trim()
|
||||
if (trimmedQuery.length === 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Query cannot be empty',
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
export function sanitizeLabelName(name: string): string {
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)) {
|
||||
throw new Error(
|
||||
'Invalid label name. Must start with a letter and contain only letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export function sanitizePropertyKey(key: string): string {
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
|
||||
throw new Error(
|
||||
'Invalid property key. Must start with a letter and contain only letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
export function sanitizeRelationshipType(type: string): string {
|
||||
if (!/^[A-Z][A-Z0-9_]*$/.test(type)) {
|
||||
throw new Error(
|
||||
'Invalid relationship type. Must start with an uppercase letter and contain only uppercase letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
export function convertNeo4jTypesToJSON(value: unknown): unknown {
|
||||
if (value === null || value === undefined) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && 'toNumber' in value) {
|
||||
return (value as any).toNumber()
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(convertNeo4jTypesToJSON)
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const obj = value as any
|
||||
|
||||
if (obj.labels && obj.properties && obj.identity) {
|
||||
return {
|
||||
identity: obj.identity.toNumber ? obj.identity.toNumber() : obj.identity,
|
||||
labels: obj.labels,
|
||||
properties: convertNeo4jTypesToJSON(obj.properties),
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.type && obj.properties && obj.identity && obj.start && obj.end) {
|
||||
return {
|
||||
identity: obj.identity.toNumber ? obj.identity.toNumber() : obj.identity,
|
||||
start: obj.start.toNumber ? obj.start.toNumber() : obj.start,
|
||||
end: obj.end.toNumber ? obj.end.toNumber() : obj.end,
|
||||
type: obj.type,
|
||||
properties: convertNeo4jTypesToJSON(obj.properties),
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.start && obj.end && obj.segments) {
|
||||
return {
|
||||
start: convertNeo4jTypesToJSON(obj.start),
|
||||
end: convertNeo4jTypesToJSON(obj.end),
|
||||
segments: obj.segments.map((seg: any) => ({
|
||||
start: convertNeo4jTypesToJSON(seg.start),
|
||||
relationship: convertNeo4jTypesToJSON(seg.relationship),
|
||||
end: convertNeo4jTypesToJSON(seg.end),
|
||||
})),
|
||||
length: obj.length,
|
||||
}
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
result[key] = convertNeo4jTypesToJSON(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -13,7 +13,6 @@ const logger = createLogger('UserSettingsAPI')
|
||||
const SettingsSchema = z.object({
|
||||
theme: z.enum(['system', 'light', 'dark']).optional(),
|
||||
autoConnect: z.boolean().optional(),
|
||||
autoFillEnvVars: z.boolean().optional(), // DEPRECATED: kept for backwards compatibility
|
||||
autoPan: z.boolean().optional(),
|
||||
consoleExpandedByDefault: z.boolean().optional(),
|
||||
telemetryEnabled: z.boolean().optional(),
|
||||
@@ -29,13 +28,13 @@ const SettingsSchema = z.object({
|
||||
showFloatingControls: z.boolean().optional(),
|
||||
showTrainingControls: z.boolean().optional(),
|
||||
superUserModeEnabled: z.boolean().optional(),
|
||||
errorNotificationsEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// Default settings values
|
||||
const defaultSettings = {
|
||||
theme: 'system',
|
||||
autoConnect: true,
|
||||
autoFillEnvVars: true, // DEPRECATED: kept for backwards compatibility, always true
|
||||
autoPan: true,
|
||||
consoleExpandedByDefault: true,
|
||||
telemetryEnabled: true,
|
||||
@@ -44,6 +43,7 @@ const defaultSettings = {
|
||||
showFloatingControls: true,
|
||||
showTrainingControls: false,
|
||||
superUserModeEnabled: false,
|
||||
errorNotificationsEnabled: true,
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
@@ -72,7 +72,6 @@ export async function GET() {
|
||||
data: {
|
||||
theme: userSettings.theme,
|
||||
autoConnect: userSettings.autoConnect,
|
||||
autoFillEnvVars: userSettings.autoFillEnvVars, // DEPRECATED: kept for backwards compatibility
|
||||
autoPan: userSettings.autoPan,
|
||||
consoleExpandedByDefault: userSettings.consoleExpandedByDefault,
|
||||
telemetryEnabled: userSettings.telemetryEnabled,
|
||||
@@ -81,6 +80,7 @@ export async function GET() {
|
||||
showFloatingControls: userSettings.showFloatingControls ?? true,
|
||||
showTrainingControls: userSettings.showTrainingControls ?? false,
|
||||
superUserModeEnabled: userSettings.superUserModeEnabled ?? true,
|
||||
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
/**
|
||||
* Tests for Subscription Transfer API
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createMockRequest,
|
||||
mockAdminMember,
|
||||
mockDb,
|
||||
mockLogger,
|
||||
mockOrganization,
|
||||
mockRegularMember,
|
||||
mockSubscription,
|
||||
mockUser,
|
||||
} from '@/app/api/__test-utils__/utils'
|
||||
|
||||
describe('Subscription Transfer API Routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: mockUser,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/logs/console/logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([mockSubscription]),
|
||||
})
|
||||
|
||||
mockDb.update.mockReturnValue({
|
||||
set: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockResolvedValue([{ affected: 1 }]),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('POST handler', () => {
|
||||
it('should successfully transfer a personal subscription to an organization', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: {
|
||||
...mockUser,
|
||||
id: 'user-123',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@sim/db/schema', () => ({
|
||||
subscription: { id: 'id', referenceId: 'referenceId' },
|
||||
organization: { id: 'id' },
|
||||
member: { userId: 'userId', organizationId: 'organizationId', role: 'role' },
|
||||
}))
|
||||
|
||||
const mockSubscriptionWithReferenceId = {
|
||||
...mockSubscription,
|
||||
referenceId: 'user-123',
|
||||
}
|
||||
|
||||
mockDb.select.mockImplementation(() => {
|
||||
return {
|
||||
from: () => ({
|
||||
where: () => {
|
||||
if (mockDb.select.mock.calls.length === 1) {
|
||||
return Promise.resolve([mockSubscriptionWithReferenceId])
|
||||
}
|
||||
if (mockDb.select.mock.calls.length === 2) {
|
||||
return Promise.resolve([mockOrganization])
|
||||
}
|
||||
return Promise.resolve([mockAdminMember])
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
mockDb.update.mockReturnValue({
|
||||
set: () => ({
|
||||
where: () => Promise.resolve({ affected: 1 }),
|
||||
}),
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('success', true)
|
||||
expect(data).toHaveProperty('message', 'Subscription transferred successfully')
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should test behavior when subscription not found', async () => {
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([]),
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
|
||||
})
|
||||
|
||||
it('should test behavior when organization not found', async () => {
|
||||
const mockSelectImpl = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([mockSubscription]),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([]),
|
||||
})
|
||||
|
||||
mockDb.select.mockImplementation(mockSelectImpl)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
|
||||
})
|
||||
|
||||
it('should reject transfer if user is not the subscription owner', async () => {
|
||||
const differentOwnerSubscription = {
|
||||
...mockSubscription,
|
||||
referenceId: 'different-user-123',
|
||||
}
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([differentOwnerSubscription]),
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
|
||||
expect(mockDb.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject non-personal transfer if user is not admin of organization', async () => {
|
||||
const orgOwnedSubscription = {
|
||||
...mockSubscription,
|
||||
referenceId: 'other-org-789',
|
||||
}
|
||||
|
||||
const mockSelectImpl = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([orgOwnedSubscription]),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([mockOrganization]),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockResolvedValue([mockRegularMember]),
|
||||
})
|
||||
|
||||
mockDb.select.mockImplementation(mockSelectImpl)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user')
|
||||
expect(mockDb.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reject invalid request parameters', async () => {
|
||||
const req = createMockRequest('POST', {})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toHaveProperty('error', 'Invalid request parameters')
|
||||
expect(mockDb.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle authentication error', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toHaveProperty('error', 'Unauthorized')
|
||||
expect(mockDb.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle internal server error', async () => {
|
||||
mockDb.select.mockImplementation(() => {
|
||||
throw new Error('Database error')
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
organizationId: 'org-456',
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route')
|
||||
|
||||
const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) })
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data).toHaveProperty('error', 'Failed to transfer subscription')
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -81,9 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
.where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId)))
|
||||
.then((rows) => rows[0])
|
||||
|
||||
const isPersonalTransfer = sub.referenceId === session.user.id
|
||||
|
||||
if (!isPersonalTransfer && (!mem || (mem.role !== 'owner' && mem.role !== 'admin'))) {
|
||||
if (!mem || (mem.role !== 'owner' && mem.role !== 'admin')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized - user is not admin of organization' },
|
||||
{ status: 403 }
|
||||
|
||||
@@ -137,7 +137,7 @@ export async function POST(request: NextRequest) {
|
||||
const isCredentialBased = credentialBasedProviders.includes(provider)
|
||||
// Treat Microsoft Teams chat subscription as credential-based for path generation purposes
|
||||
const isMicrosoftTeamsChatSubscription =
|
||||
provider === 'microsoftteams' &&
|
||||
provider === 'microsoft-teams' &&
|
||||
typeof providerConfig === 'object' &&
|
||||
providerConfig?.triggerId === 'microsoftteams_chat_subscription'
|
||||
|
||||
@@ -297,7 +297,32 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'microsoftteams') {
|
||||
if (provider === 'calendly') {
|
||||
logger.info(`[${requestId}] Creating Calendly subscription before saving to database`)
|
||||
try {
|
||||
externalSubscriptionId = await createCalendlyWebhookSubscription(
|
||||
request,
|
||||
userId,
|
||||
createTempWebhookData(),
|
||||
requestId
|
||||
)
|
||||
if (externalSubscriptionId) {
|
||||
resolvedProviderConfig.externalId = externalSubscriptionId
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[${requestId}] Error creating Calendly webhook subscription`, err)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create webhook in Calendly',
|
||||
details: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'microsoft-teams') {
|
||||
const { createTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers')
|
||||
logger.info(`[${requestId}] Creating Teams subscription before saving to database`)
|
||||
try {
|
||||
@@ -635,6 +660,140 @@ async function createAirtableWebhookSubscription(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the webhook subscription in Calendly
|
||||
async function createCalendlyWebhookSubscription(
|
||||
request: NextRequest,
|
||||
userId: string,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { apiKey, organization, triggerId } = providerConfig || {}
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn(`[${requestId}] Missing apiKey for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Personal Access Token is required to create Calendly webhook. Please provide your Calendly Personal Access Token.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
logger.warn(`[${requestId}] Missing organization URI for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Organization URI is required to create Calendly webhook. Please provide your Organization URI from the "Get Current User" operation.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!triggerId) {
|
||||
logger.warn(`[${requestId}] Missing triggerId for Calendly webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error('Trigger ID is required to create Calendly webhook')
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
// Map trigger IDs to Calendly event types
|
||||
const eventTypeMap: Record<string, string[]> = {
|
||||
calendly_invitee_created: ['invitee.created'],
|
||||
calendly_invitee_canceled: ['invitee.canceled'],
|
||||
calendly_routing_form_submitted: ['routing_form_submission.created'],
|
||||
calendly_webhook: ['invitee.created', 'invitee.canceled', 'routing_form_submission.created'],
|
||||
}
|
||||
|
||||
const events = eventTypeMap[triggerId] || ['invitee.created']
|
||||
|
||||
const calendlyApiUrl = 'https://api.calendly.com/webhook_subscriptions'
|
||||
|
||||
const requestBody = {
|
||||
url: notificationUrl,
|
||||
events,
|
||||
organization,
|
||||
scope: 'organization',
|
||||
}
|
||||
|
||||
const calendlyResponse = await fetch(calendlyApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!calendlyResponse.ok) {
|
||||
const errorBody = await calendlyResponse.json().catch(() => ({}))
|
||||
const errorMessage = errorBody.message || errorBody.title || 'Unknown Calendly API error'
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create webhook in Calendly for webhook ${webhookData.id}. Status: ${calendlyResponse.status}`,
|
||||
{ response: errorBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Calendly'
|
||||
if (calendlyResponse.status === 401) {
|
||||
userFriendlyMessage =
|
||||
'Calendly authentication failed. Please verify your Personal Access Token is correct.'
|
||||
} else if (calendlyResponse.status === 403) {
|
||||
userFriendlyMessage =
|
||||
'Calendly access denied. Please ensure you have appropriate permissions and a paid Calendly subscription.'
|
||||
} else if (calendlyResponse.status === 404) {
|
||||
userFriendlyMessage =
|
||||
'Calendly organization not found. Please verify the Organization URI is correct.'
|
||||
} else if (errorMessage && errorMessage !== 'Unknown Calendly API error') {
|
||||
userFriendlyMessage = `Calendly error: ${errorMessage}`
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
const responseBody = await calendlyResponse.json()
|
||||
const webhookUri = responseBody.resource?.uri
|
||||
|
||||
if (!webhookUri) {
|
||||
logger.error(
|
||||
`[${requestId}] Calendly webhook created but no webhook URI returned for webhook ${webhookData.id}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
throw new Error('Calendly webhook creation succeeded but no webhook URI was returned')
|
||||
}
|
||||
|
||||
// Extract the webhook ID from the URI (e.g., https://api.calendly.com/webhook_subscriptions/WEBHOOK_ID)
|
||||
const webhookId = webhookUri.split('/').pop()
|
||||
|
||||
if (!webhookId) {
|
||||
logger.error(`[${requestId}] Could not extract webhook ID from Calendly URI: ${webhookUri}`, {
|
||||
response: responseBody,
|
||||
})
|
||||
throw new Error('Failed to extract webhook ID from Calendly response')
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created webhook in Calendly for webhook ${webhookData.id}.`,
|
||||
{
|
||||
calendlyWebhookUri: webhookUri,
|
||||
calendlyWebhookId: webhookId,
|
||||
}
|
||||
)
|
||||
return webhookId
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Exception during Calendly webhook creation for webhook ${webhookData.id}.`,
|
||||
{
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
)
|
||||
// Re-throw the error so it can be caught by the outer try-catch
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the webhook subscription in Webflow
|
||||
async function createWebflowWebhookSubscription(
|
||||
request: NextRequest,
|
||||
|
||||
@@ -441,7 +441,7 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
case 'microsoftteams': {
|
||||
case 'microsoft-teams': {
|
||||
const hmacSecret = providerConfig.hmacSecret
|
||||
|
||||
if (!hmacSecret) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import {
|
||||
checkRateLimits,
|
||||
@@ -139,34 +137,10 @@ export async function POST(
|
||||
if (foundWebhook.blockId) {
|
||||
const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId)
|
||||
if (!blockExists) {
|
||||
logger.warn(
|
||||
logger.info(
|
||||
`[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}`
|
||||
)
|
||||
|
||||
const executionId = uuidv4()
|
||||
const loggingSession = new LoggingSession(foundWorkflow.id, executionId, 'webhook', requestId)
|
||||
|
||||
const actorUserId = foundWorkflow.workspaceId
|
||||
? (await import('@/lib/workspaces/utils')).getWorkspaceBilledAccountUserId(
|
||||
foundWorkflow.workspaceId
|
||||
) || foundWorkflow.userId
|
||||
: foundWorkflow.userId
|
||||
|
||||
await loggingSession.safeStart({
|
||||
userId: actorUserId,
|
||||
workspaceId: foundWorkflow.workspaceId || '',
|
||||
variables: {},
|
||||
})
|
||||
|
||||
await loggingSession.safeCompleteWithError({
|
||||
error: {
|
||||
message: `Trigger block not deployed. The webhook trigger (block ${foundWebhook.blockId}) is not present in the deployed workflow. Please redeploy the workflow.`,
|
||||
stackTrace: undefined,
|
||||
},
|
||||
traceSpans: [],
|
||||
})
|
||||
|
||||
return new NextResponse('Trigger block not deployed', { status: 404 })
|
||||
return new NextResponse('Trigger block not found in deployment', { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
import { createStreamingResponse } from '@/lib/workflows/streaming'
|
||||
import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
@@ -495,6 +496,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
loggingSession,
|
||||
})
|
||||
|
||||
const hasResponseBlock = workflowHasResponseBlock(result)
|
||||
if (hasResponseBlock) {
|
||||
return createHttpResponseFromBlock(result)
|
||||
}
|
||||
|
||||
const filteredResult = {
|
||||
success: result.success,
|
||||
output: result.output,
|
||||
@@ -571,10 +577,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
if (isStreamClosed) return
|
||||
|
||||
try {
|
||||
logger.info(`[${requestId}] 📤 Sending SSE event:`, {
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
})
|
||||
controller.enqueue(encodeSSEEvent(event))
|
||||
} catch {
|
||||
isStreamClosed = true
|
||||
|
||||
@@ -118,18 +118,18 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
|
||||
|
||||
// Track workflow creation
|
||||
try {
|
||||
const { trackPlatformEvent } = await import('@/lib/telemetry/tracer')
|
||||
trackPlatformEvent('platform.workflow.created', {
|
||||
'workflow.id': workflowId,
|
||||
'workflow.name': name,
|
||||
'workflow.has_workspace': !!workspaceId,
|
||||
'workflow.has_folder': !!folderId,
|
||||
import('@/lib/telemetry/tracer')
|
||||
.then(({ trackPlatformEvent }) => {
|
||||
trackPlatformEvent('platform.workflow.created', {
|
||||
'workflow.id': workflowId,
|
||||
'workflow.name': name,
|
||||
'workflow.has_workspace': !!workspaceId,
|
||||
'workflow.has_folder': !!folderId,
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail
|
||||
})
|
||||
} catch (_e) {
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
await db.insert(workflow).values({
|
||||
id: workflowId,
|
||||
|
||||
@@ -74,30 +74,6 @@
|
||||
animation: dash-animation 1.5s linear infinite !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active block ring animation - cycles through gray tones using box-shadow
|
||||
*/
|
||||
@keyframes ring-pulse-colors {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 4px var(--surface-14);
|
||||
}
|
||||
33% {
|
||||
box-shadow: 0 0 0 4px var(--surface-12);
|
||||
}
|
||||
66% {
|
||||
box-shadow: 0 0 0 4px var(--surface-15);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .animate-ring-pulse {
|
||||
animation: ring-pulse-colors 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
.light .animate-ring-pulse {
|
||||
animation: ring-pulse-colors 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark color tokens - single source of truth for all colors (dark-only)
|
||||
*/
|
||||
@@ -135,6 +111,7 @@
|
||||
--border-strong: #d1d1d1;
|
||||
--divider: #e5e5e5;
|
||||
--border-muted: #eeeeee;
|
||||
--border-success: #d5d5d5;
|
||||
|
||||
/* Brand & state */
|
||||
--brand-400: #8e4cfb;
|
||||
@@ -250,6 +227,7 @@
|
||||
--border-strong: #303030;
|
||||
--divider: #393939;
|
||||
--border-muted: #424242;
|
||||
--border-success: #575757;
|
||||
|
||||
/* Brand & state */
|
||||
--brand-400: #8e4cfb;
|
||||
@@ -303,6 +281,8 @@
|
||||
--c-F4F4F4: #f4f4f4;
|
||||
--c-F5F5F5: #f5f5f5;
|
||||
|
||||
--c-CFCFCF: #cfcfcf;
|
||||
|
||||
/* Blues and cyans */
|
||||
--c-00B0B0: #00b0b0;
|
||||
--c-264F78: #264f78;
|
||||
|
||||
@@ -4,45 +4,56 @@ import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = 'https://sim.ai'
|
||||
|
||||
const staticPages = [
|
||||
const now = new Date()
|
||||
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily' as const,
|
||||
priority: 1,
|
||||
lastModified: now,
|
||||
priority: 1.0, // Homepage - highest priority
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/signup`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' as const,
|
||||
priority: 0.9,
|
||||
url: `${baseUrl}/studio`,
|
||||
lastModified: now,
|
||||
priority: 0.9, // Blog index - high value content
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/login`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.8,
|
||||
url: `${baseUrl}/studio/tags`,
|
||||
lastModified: now,
|
||||
priority: 0.7, // Tags page - discovery/navigation
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/templates`,
|
||||
lastModified: now,
|
||||
priority: 0.8, // Templates - important discovery page
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/changelog`,
|
||||
lastModified: now,
|
||||
priority: 0.8, // Changelog - important for users
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/careers`,
|
||||
lastModified: new Date('2024-10-06'),
|
||||
priority: 0.6, // Careers - important but not core content
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/terms`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.5,
|
||||
lastModified: new Date('2024-10-14'),
|
||||
priority: 0.5, // Terms - utility page
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/privacy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.5,
|
||||
lastModified: new Date('2024-10-14'),
|
||||
priority: 0.5, // Privacy - utility page
|
||||
},
|
||||
]
|
||||
|
||||
const posts = await getAllPostMeta()
|
||||
const blogPages = posts.map((p) => ({
|
||||
const blogPages: MetadataRoute.Sitemap = posts.map((p) => ({
|
||||
url: p.canonical,
|
||||
lastModified: new Date(p.updated ?? p.date),
|
||||
changeFrequency: 'monthly' as const,
|
||||
priority: 0.9 as const,
|
||||
priority: 0.9, // Blog posts - high value content
|
||||
}))
|
||||
|
||||
return [...staticPages, ...blogPages]
|
||||
|
||||
@@ -34,9 +34,9 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { CredentialRequirement } from '@/lib/workflows/credential-extractor'
|
||||
import type { Template } from '@/app/templates/templates'
|
||||
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { useStarTemplate, useTemplate } from '@/hooks/queries/templates'
|
||||
|
||||
const logger = createLogger('TemplateDetails')
|
||||
|
||||
@@ -52,16 +52,14 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
const workspaceId = isWorkspaceContext ? (params?.workspaceId as string) : null
|
||||
const { data: session } = useSession()
|
||||
|
||||
const [template, setTemplate] = useState<Template | null>(null)
|
||||
const { data: template, isLoading: loading } = useTemplate(templateId)
|
||||
const starTemplate = useStarTemplate()
|
||||
|
||||
const [currentUserOrgs, setCurrentUserOrgs] = useState<string[]>([])
|
||||
const [currentUserOrgRoles, setCurrentUserOrgRoles] = useState<
|
||||
Array<{ organizationId: string; role: string }>
|
||||
>([])
|
||||
const [isSuperUser, setIsSuperUser] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isStarred, setIsStarred] = useState(false)
|
||||
const [starCount, setStarCount] = useState(0)
|
||||
const [isStarring, setIsStarring] = useState(false)
|
||||
const [isUsing, setIsUsing] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isApproving, setIsApproving] = useState(false)
|
||||
@@ -76,29 +74,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
|
||||
const currentUserId = session?.user?.id || null
|
||||
|
||||
// Fetch template data on client side
|
||||
useEffect(() => {
|
||||
if (!templateId) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const fetchTemplate = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/templates/${templateId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setTemplate(data.data)
|
||||
setIsStarred(data.data.isStarred || false)
|
||||
setStarCount(data.data.stars || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching template:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUserOrganizations = async () => {
|
||||
if (!currentUserId) return
|
||||
|
||||
@@ -134,12 +110,10 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
}
|
||||
}
|
||||
|
||||
fetchTemplate()
|
||||
fetchSuperUserStatus()
|
||||
fetchUserOrganizations()
|
||||
}, [templateId, currentUserId])
|
||||
}, [currentUserId])
|
||||
|
||||
// Fetch workspaces when user is logged in
|
||||
useEffect(() => {
|
||||
if (!currentUserId) return
|
||||
|
||||
@@ -149,7 +123,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
const response = await fetch('/api/workspaces')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Filter workspaces where user has write/admin permissions
|
||||
const availableWorkspaces = data.workspaces
|
||||
.filter((ws: any) => ws.permissions === 'write' || ws.permissions === 'admin')
|
||||
.map((ws: any) => ({
|
||||
@@ -169,7 +142,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
fetchWorkspaces()
|
||||
}, [currentUserId])
|
||||
|
||||
// Clean up URL when returning from login
|
||||
useEffect(() => {
|
||||
if (template && searchParams?.get('use') === 'true' && currentUserId) {
|
||||
if (isWorkspaceContext && workspaceId) {
|
||||
@@ -181,26 +153,20 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
}
|
||||
}, [searchParams, currentUserId, template, isWorkspaceContext, workspaceId, router])
|
||||
|
||||
// Check if user can edit template
|
||||
const canEditTemplate = (() => {
|
||||
if (!currentUserId || !template?.creator) return false
|
||||
|
||||
// For user creator profiles: must be the user themselves
|
||||
if (template.creator.referenceType === 'user') {
|
||||
return template.creator.referenceId === currentUserId
|
||||
}
|
||||
|
||||
// For organization creator profiles:
|
||||
if (template.creator.referenceType === 'organization' && template.creator.referenceId) {
|
||||
const isOrgMember = currentUserOrgs.includes(template.creator.referenceId)
|
||||
|
||||
// If template has a connected workflow, any org member with workspace access can edit
|
||||
if (template.workflowId) {
|
||||
return isOrgMember
|
||||
}
|
||||
|
||||
// If template is orphaned, only admin/owner can edit
|
||||
// We need to check the user's role in the organization
|
||||
const orgMembership = currentUserOrgRoles.find(
|
||||
(org) => org.organizationId === template.creator?.referenceId
|
||||
)
|
||||
@@ -212,7 +178,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
return false
|
||||
})()
|
||||
|
||||
// Check workspace access for connected workflow
|
||||
useEffect(() => {
|
||||
const checkWorkspaceAccess = async () => {
|
||||
if (!template?.workflowId || !currentUserId || !canEditTemplate) {
|
||||
@@ -227,7 +192,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
} else if (checkResponse.ok) {
|
||||
setHasWorkspaceAccess(true)
|
||||
} else {
|
||||
// Workflow doesn't exist
|
||||
setHasWorkspaceAccess(null)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -319,32 +283,20 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
* @param event - The wheel event fired when the user scrolls over the preview area.
|
||||
*/
|
||||
const handleCanvasWheelCapture = (event: React.WheelEvent<HTMLDivElement>) => {
|
||||
// Allow pinch/zoom gestures (e.g., ctrl/cmd + wheel) to continue to the canvas.
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent React Flow from handling the wheel; let the page scroll naturally.
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const handleStarToggle = async () => {
|
||||
if (isStarring || !currentUserId) return
|
||||
if (!currentUserId || !template) return
|
||||
|
||||
setIsStarring(true)
|
||||
try {
|
||||
const method = isStarred ? 'DELETE' : 'POST'
|
||||
const response = await fetch(`/api/templates/${template.id}/star`, { method })
|
||||
|
||||
if (response.ok) {
|
||||
setIsStarred(!isStarred)
|
||||
setStarCount((prev) => (isStarred ? prev - 1 : prev + 1))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error toggling star:', error)
|
||||
} finally {
|
||||
setIsStarring(false)
|
||||
}
|
||||
starTemplate.mutate({
|
||||
templateId: template.id,
|
||||
action: template.isStarred ? 'remove' : 'add',
|
||||
})
|
||||
}
|
||||
|
||||
const handleUseTemplate = () => {
|
||||
@@ -357,7 +309,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
return
|
||||
}
|
||||
|
||||
// In workspace context, use current workspace directly
|
||||
if (isWorkspaceContext && workspaceId) {
|
||||
handleWorkspaceSelectForUse(workspaceId)
|
||||
}
|
||||
@@ -366,7 +317,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
const handleEditTemplate = async () => {
|
||||
if (!currentUserId || !template) return
|
||||
|
||||
// In workspace context with existing workflow, navigate directly
|
||||
if (isWorkspaceContext && workspaceId && template.workflowId) {
|
||||
setIsEditing(true)
|
||||
try {
|
||||
@@ -381,10 +331,8 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
} finally {
|
||||
setIsEditing(false)
|
||||
}
|
||||
// If workflow doesn't exist, fall through to workspace selector
|
||||
}
|
||||
|
||||
// Check if workflow exists and user has access (global context)
|
||||
if (template.workflowId && !isWorkspaceContext) {
|
||||
setIsEditing(true)
|
||||
try {
|
||||
@@ -410,7 +358,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
}
|
||||
}
|
||||
|
||||
// Workflow doesn't exist - show workspace selector or use current workspace
|
||||
if (isWorkspaceContext && workspaceId) {
|
||||
handleWorkspaceSelectForEdit(workspaceId)
|
||||
} else {
|
||||
@@ -435,7 +382,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
|
||||
const { workflowId } = await response.json()
|
||||
|
||||
// Navigate to the new workflow with full page load
|
||||
window.location.href = `/workspace/${workspaceId}/w/${workflowId}`
|
||||
} catch (error) {
|
||||
logger.error('Error using template:', error)
|
||||
@@ -450,7 +396,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
setIsUsing(true)
|
||||
setShowWorkspaceSelectorForEdit(false)
|
||||
try {
|
||||
// Import template as a new workflow and connect it to the template
|
||||
const response = await fetch(`/api/templates/${template.id}/use`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -463,7 +408,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
|
||||
const { workflowId } = await response.json()
|
||||
|
||||
// Navigate to the new workflow with full page load
|
||||
window.location.href = `/workspace/${workspaceId}/w/${workflowId}`
|
||||
} catch (error) {
|
||||
logger.error('Error importing template for editing:', error)
|
||||
@@ -482,9 +426,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Update template status optimistically
|
||||
setTemplate({ ...template, status: 'approved' })
|
||||
// Redirect back to templates page after approval
|
||||
if (isWorkspaceContext && workspaceId) {
|
||||
router.push(`/workspace/${workspaceId}/templates`)
|
||||
} else {
|
||||
@@ -508,9 +449,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Update template status optimistically
|
||||
setTemplate({ ...template, status: 'rejected' })
|
||||
// Redirect back to templates page after rejection
|
||||
if (isWorkspaceContext && workspaceId) {
|
||||
router.push(`/workspace/${workspaceId}/templates`)
|
||||
} else {
|
||||
@@ -752,11 +690,11 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
|
||||
onClick={handleStarToggle}
|
||||
className={cn(
|
||||
'h-[14px] w-[14px] cursor-pointer transition-colors',
|
||||
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
|
||||
isStarring && 'opacity-50'
|
||||
template.isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
|
||||
starTemplate.isPending && 'opacity-50'
|
||||
)}
|
||||
/>
|
||||
<span className='font-medium text-[#888888] text-[14px]'>{starCount}</span>
|
||||
<span className='font-medium text-[#888888] text-[14px]'>{template.stars || 0}</span>
|
||||
|
||||
{/* Users icon and count */}
|
||||
<ChartNoAxesColumn className='h-[16px] w-[16px] text-[#888888]' />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { useStarTemplate } from '@/hooks/queries/templates'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('TemplateCard')
|
||||
@@ -12,37 +13,20 @@ const logger = createLogger('TemplateCard')
|
||||
interface TemplateCardProps {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
author: string
|
||||
authorImageUrl?: string | null
|
||||
usageCount: string
|
||||
stars?: number
|
||||
icon?: React.ReactNode | string
|
||||
iconColor?: string
|
||||
blocks?: string[]
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
// Workflow state for rendering preview
|
||||
state?: WorkflowState
|
||||
isStarred?: boolean
|
||||
// Optional callback when template is successfully used (for closing modals, etc.)
|
||||
onTemplateUsed?: () => void
|
||||
// Callback when star state changes (for parent state updates)
|
||||
onStarChange?: (templateId: string, isStarred: boolean, newStarCount: number) => void
|
||||
// User authentication status
|
||||
isAuthenticated?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton component for loading states
|
||||
*/
|
||||
export function TemplateCardSkeleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('h-[268px] w-full rounded-[8px] bg-[#202020] p-[8px]', className)}>
|
||||
{/* Workflow preview skeleton */}
|
||||
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-gray-700' />
|
||||
|
||||
{/* Title and blocks row skeleton */}
|
||||
<div className='mt-[14px] flex items-center justify-between'>
|
||||
<div className='h-4 w-32 animate-pulse rounded bg-gray-700' />
|
||||
<div className='flex items-center gap-[-4px]'>
|
||||
@@ -55,7 +39,6 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Creator and stats row skeleton */}
|
||||
<div className='mt-[14px] flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<div className='h-[14px] w-[14px] animate-pulse rounded-full bg-gray-700' />
|
||||
@@ -72,31 +55,23 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Utility function to extract block types from workflow state
|
||||
const extractBlockTypesFromState = (state?: {
|
||||
blocks?: Record<string, { type: string; name?: string }>
|
||||
}): string[] => {
|
||||
if (!state?.blocks) return []
|
||||
|
||||
// Get unique block types from the state, excluding starter blocks
|
||||
// Sort the keys to ensure consistent ordering between server and client
|
||||
const blockTypes = Object.keys(state.blocks)
|
||||
.sort() // Sort keys to ensure consistent order
|
||||
.sort()
|
||||
.map((key) => state.blocks![key].type)
|
||||
.filter((type) => type !== 'starter')
|
||||
return [...new Set(blockTypes)]
|
||||
}
|
||||
|
||||
// Utility function to get the full block config for colored icon display
|
||||
const getBlockConfig = (blockType: string) => {
|
||||
const block = getBlock(blockType)
|
||||
return block
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an arbitrary workflow-like object into a valid WorkflowState for preview rendering.
|
||||
* Ensures required fields exist: blocks with required properties, edges array, loops and parallels maps.
|
||||
*/
|
||||
function normalizeWorkflowState(input?: any): WorkflowState | null {
|
||||
if (!input || !input.blocks) return null
|
||||
|
||||
@@ -142,34 +117,22 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
|
||||
function TemplateCardInner({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
author,
|
||||
authorImageUrl,
|
||||
usageCount,
|
||||
stars = 0,
|
||||
icon,
|
||||
iconColor = 'bg-blue-500',
|
||||
blocks = [],
|
||||
onClick,
|
||||
className,
|
||||
state,
|
||||
isStarred = false,
|
||||
onTemplateUsed,
|
||||
onStarChange,
|
||||
isAuthenticated = true,
|
||||
}: TemplateCardProps) {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
|
||||
// Local state for optimistic updates
|
||||
const [localIsStarred, setLocalIsStarred] = useState(isStarred)
|
||||
const [localStarCount, setLocalStarCount] = useState(stars)
|
||||
const [isStarLoading, setIsStarLoading] = useState(false)
|
||||
const { mutate: toggleStar, isPending: isStarLoading } = useStarTemplate()
|
||||
|
||||
// Memoize normalized workflow state to avoid recalculation on every render
|
||||
const normalizedState = useMemo(() => normalizeWorkflowState(state), [state])
|
||||
|
||||
// Use IntersectionObserver to defer rendering the heavy WorkflowPreview until in viewport
|
||||
const previewRef = useRef<HTMLDivElement | null>(null)
|
||||
const [isInView, setIsInView] = useState(false)
|
||||
|
||||
@@ -188,9 +151,6 @@ function TemplateCardInner({
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
// Extract block types from state if provided, otherwise use the blocks prop
|
||||
// Filter out starter blocks in both cases and sort for consistent rendering
|
||||
// Memoized to prevent recalculation on every render
|
||||
const blockTypes = useMemo(
|
||||
() =>
|
||||
state
|
||||
@@ -199,65 +159,16 @@ function TemplateCardInner({
|
||||
[state, blocks]
|
||||
)
|
||||
|
||||
// Handle star toggle with optimistic updates
|
||||
const handleStarClick = async (e: React.MouseEvent) => {
|
||||
const handleStarClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
// Prevent multiple clicks while loading
|
||||
if (isStarLoading) return
|
||||
|
||||
setIsStarLoading(true)
|
||||
|
||||
// Optimistic update - update UI immediately
|
||||
const newIsStarred = !localIsStarred
|
||||
const newStarCount = newIsStarred ? localStarCount + 1 : localStarCount - 1
|
||||
|
||||
setLocalIsStarred(newIsStarred)
|
||||
setLocalStarCount(newStarCount)
|
||||
|
||||
// Notify parent component immediately for optimistic update
|
||||
if (onStarChange) {
|
||||
onStarChange(id, newIsStarred, newStarCount)
|
||||
}
|
||||
|
||||
try {
|
||||
const method = localIsStarred ? 'DELETE' : 'POST'
|
||||
const response = await fetch(`/api/templates/${id}/star`, { method })
|
||||
|
||||
if (!response.ok) {
|
||||
// Rollback on error
|
||||
setLocalIsStarred(localIsStarred)
|
||||
setLocalStarCount(localStarCount)
|
||||
|
||||
// Rollback parent state too
|
||||
if (onStarChange) {
|
||||
onStarChange(id, localIsStarred, localStarCount)
|
||||
}
|
||||
|
||||
logger.error('Failed to toggle star:', response.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
// Rollback on error
|
||||
setLocalIsStarred(localIsStarred)
|
||||
setLocalStarCount(localStarCount)
|
||||
|
||||
// Rollback parent state too
|
||||
if (onStarChange) {
|
||||
onStarChange(id, localIsStarred, localStarCount)
|
||||
}
|
||||
|
||||
logger.error('Error toggling star:', error)
|
||||
} finally {
|
||||
setIsStarLoading(false)
|
||||
}
|
||||
toggleStar({
|
||||
templateId: id,
|
||||
action: isStarred ? 'remove' : 'add',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate template detail page URL based on context.
|
||||
* If we're in a workspace context, navigate to the workspace template page.
|
||||
* Otherwise, navigate to the global template page.
|
||||
* Memoized to avoid recalculation on every render.
|
||||
*/
|
||||
const templateUrl = useMemo(() => {
|
||||
const workspaceId = params?.workspaceId as string | undefined
|
||||
if (workspaceId) {
|
||||
@@ -266,23 +177,8 @@ function TemplateCardInner({
|
||||
return `/templates/${id}`
|
||||
}, [params?.workspaceId, id])
|
||||
|
||||
/**
|
||||
* Handle use button click - navigate to template detail page
|
||||
*/
|
||||
const handleUseClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
router.push(templateUrl)
|
||||
},
|
||||
[router, templateUrl]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle card click - navigate to template detail page
|
||||
*/
|
||||
const handleCardClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Don't navigate if clicking on action buttons
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('button') || target.closest('[data-action]')) {
|
||||
return
|
||||
@@ -298,7 +194,6 @@ function TemplateCardInner({
|
||||
onClick={handleCardClick}
|
||||
className={cn('w-full cursor-pointer rounded-[8px] bg-[#202020] p-[8px]', className)}
|
||||
>
|
||||
{/* Workflow Preview */}
|
||||
<div
|
||||
ref={previewRef}
|
||||
className='pointer-events-none h-[180px] w-full overflow-hidden rounded-[6px]'
|
||||
@@ -318,16 +213,12 @@ function TemplateCardInner({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title and Blocks Row */}
|
||||
<div className='mt-[10px] flex items-center justify-between'>
|
||||
{/* Template Name */}
|
||||
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-white'>{title}</h3>
|
||||
|
||||
{/* Block Icons */}
|
||||
<div className='flex flex-shrink-0'>
|
||||
{blockTypes.length > 4 ? (
|
||||
<>
|
||||
{/* Show first 3 blocks when there are more than 4 */}
|
||||
{blockTypes.slice(0, 3).map((blockType, index) => {
|
||||
const blockConfig = getBlockConfig(blockType)
|
||||
if (!blockConfig) return null
|
||||
@@ -345,7 +236,6 @@ function TemplateCardInner({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Show +n for remaining blocks */}
|
||||
<div
|
||||
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[#4A4A4A]'
|
||||
style={{ marginLeft: '-4px' }}
|
||||
@@ -354,7 +244,6 @@ function TemplateCardInner({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Show all blocks when 4 or fewer */
|
||||
blockTypes.map((blockType, index) => {
|
||||
const blockConfig = getBlockConfig(blockType)
|
||||
if (!blockConfig) return null
|
||||
@@ -376,9 +265,7 @@ function TemplateCardInner({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Creator and Stats Row */}
|
||||
<div className='mt-[10px] flex items-center justify-between'>
|
||||
{/* Creator Info */}
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
{authorImageUrl ? (
|
||||
<div className='h-[26px] w-[26px] flex-shrink-0 overflow-hidden rounded-full'>
|
||||
@@ -392,7 +279,6 @@ function TemplateCardInner({
|
||||
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>
|
||||
<User className='h-[12px] w-[12px]' />
|
||||
<span>{usageCount}</span>
|
||||
@@ -400,11 +286,11 @@ function TemplateCardInner({
|
||||
onClick={handleStarClick}
|
||||
className={cn(
|
||||
'h-[12px] w-[12px] cursor-pointer transition-colors',
|
||||
localIsStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
|
||||
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
|
||||
isStarLoading && 'opacity-50'
|
||||
)}
|
||||
/>
|
||||
<span>{localStarCount}</span>
|
||||
<span>{stars}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { CredentialRequirement } from '@/lib/workflows/credential-extractor'
|
||||
import { TemplateCard, TemplateCardSkeleton } from '@/app/templates/components/template-card'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import type { CreatorProfileDetails } from '@/types/creator-profile'
|
||||
|
||||
@@ -55,11 +56,11 @@ export default function Templates({
|
||||
}: TemplatesProps) {
|
||||
const router = useRouter()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300)
|
||||
const [activeTab, setActiveTab] = useState('gallery')
|
||||
const [templates, setTemplates] = useState<Template[]>(initialTemplates)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Redirect authenticated users to workspace templates
|
||||
useEffect(() => {
|
||||
if (currentUserId) {
|
||||
const redirectToWorkspace = async () => {
|
||||
@@ -80,32 +81,19 @@ export default function Templates({
|
||||
}
|
||||
}, [currentUserId, router])
|
||||
|
||||
/**
|
||||
* Update star status for a template
|
||||
*/
|
||||
const handleStarChange = (templateId: string, isStarred: boolean, newStarCount: number) => {
|
||||
setTemplates((prevTemplates) =>
|
||||
prevTemplates.map((template) =>
|
||||
template.id === templateId ? { ...template, isStarred, stars: newStarCount } : template
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter templates based on active tab and search query
|
||||
* Memoized to prevent unnecessary recalculations on render
|
||||
*/
|
||||
const filteredTemplates = useMemo(() => {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const query = debouncedSearchQuery.toLowerCase()
|
||||
|
||||
return templates.filter((template) => {
|
||||
// Filter by tab - only gallery and pending for public page
|
||||
const tabMatch =
|
||||
activeTab === 'gallery' ? template.status === 'approved' : template.status === 'pending'
|
||||
|
||||
if (!tabMatch) return false
|
||||
|
||||
// Filter by search query
|
||||
if (!query) return true
|
||||
|
||||
const searchableText = [template.name, template.details?.tagline, template.creator?.name]
|
||||
@@ -115,14 +103,14 @@ export default function Templates({
|
||||
|
||||
return searchableText.includes(query)
|
||||
})
|
||||
}, [templates, activeTab, searchQuery])
|
||||
}, [templates, activeTab, debouncedSearchQuery])
|
||||
|
||||
/**
|
||||
* Get empty state message based on current filters
|
||||
* Memoized to prevent unnecessary recalculations on render
|
||||
*/
|
||||
const emptyState = useMemo(() => {
|
||||
if (searchQuery) {
|
||||
if (debouncedSearchQuery) {
|
||||
return {
|
||||
title: 'No templates found',
|
||||
description: 'Try a different search term',
|
||||
@@ -141,7 +129,7 @@ export default function Templates({
|
||||
}
|
||||
|
||||
return messages[activeTab as keyof typeof messages] || messages.gallery
|
||||
}, [searchQuery, activeTab])
|
||||
}, [debouncedSearchQuery, activeTab])
|
||||
|
||||
return (
|
||||
<div className='flex h-[100vh] flex-col'>
|
||||
@@ -209,15 +197,12 @@ export default function Templates({
|
||||
key={template.id}
|
||||
id={template.id}
|
||||
title={template.name}
|
||||
description={template.details?.tagline || ''}
|
||||
author={template.creator?.name || 'Unknown'}
|
||||
authorImageUrl={template.creator?.profileImageUrl || null}
|
||||
usageCount={template.views.toString()}
|
||||
stars={template.stars}
|
||||
state={template.state}
|
||||
isStarred={template.isStarred}
|
||||
onStarChange={handleStarChange}
|
||||
isAuthenticated={!!currentUserId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -2,20 +2,8 @@
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { AlertCircle, Loader2, X } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button, Modal, ModalContent, ModalTitle, Textarea } from '@/components/emcn'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
|
||||
|
||||
@@ -123,109 +111,135 @@ export function CreateChunkModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleCloseAttempt}>
|
||||
<DialogContent
|
||||
<Modal open={open} onOpenChange={handleCloseAttempt}>
|
||||
<ModalContent
|
||||
className='flex h-[74vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
hideCloseButton
|
||||
showClose={false}
|
||||
>
|
||||
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<DialogTitle className='font-medium text-lg'>Create Chunk</DialogTitle>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 p-0'
|
||||
onClick={handleCloseAttempt}
|
||||
>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Create Chunk
|
||||
</ModalTitle>
|
||||
<Button variant='ghost' className='h-8 w-8 p-0' onClick={handleCloseAttempt}>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col overflow-hidden'>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto px-6'>
|
||||
<div className='flex min-h-full flex-col py-4'>
|
||||
{/* Document Info Section - Fixed at top */}
|
||||
<div className='flex-shrink-0 space-y-4'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>Adding chunk to this document</p>
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'>
|
||||
<div className='flex min-h-full flex-col px-6'>
|
||||
<div className='flex flex-1 flex-col space-y-[12px] pt-0 pb-6'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex-shrink-0 space-y-[8px]'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Adding chunk to this document
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill space */}
|
||||
<div className='flex min-h-0 flex-1 flex-col space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='font-medium text-[13px] text-[var(--text-primary)] dark: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...'
|
||||
className='min-h-0 flex-1 resize-none'
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill remaining space */}
|
||||
<div className='mt-4 flex flex-1 flex-col'>
|
||||
<Label htmlFor='content' className='mb-2 font-medium text-sm'>
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder='Enter the content for this chunk...'
|
||||
className='flex-1 resize-none'
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
disabled={isCreating}
|
||||
/>
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreateChunk}
|
||||
type='button'
|
||||
disabled={!isFormValid || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Chunk'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='mt-auto border-t px-6 pt-4 pb-6'>
|
||||
<div className='flex justify-between'>
|
||||
<Button variant='outline' onClick={handleCloseAttempt} disabled={isCreating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateChunk}
|
||||
disabled={!isFormValid || isCreating}
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Chunk'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Unsaved Changes Alert */}
|
||||
<AlertDialog open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<ModalContent className='flex flex-col gap-0 p-0'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Discard changes?
|
||||
</ModalTitle>
|
||||
<p className='mt-2 text-[12px] text-[var(--text-secondary)] dark:text-[var(--text-secondary)]'>
|
||||
You have unsaved changes. Are you sure you want to close without saving?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setShowUnsavedChangesAlert(false)}>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => setShowUnsavedChangesAlert(false)}
|
||||
type='button'
|
||||
>
|
||||
Keep editing
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDiscard}>Discard changes</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
<Button variant='primary' onClick={handleConfirmDiscard} type='button'>
|
||||
Discard changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ChunkData } from '@/stores/knowledge/store'
|
||||
|
||||
@@ -76,20 +75,30 @@ export function DeleteChunkModal({
|
||||
if (!chunk) return null
|
||||
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Chunk</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this chunk? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
<Modal open={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Chunk</ModalTitle>
|
||||
<ModalDescription>
|
||||
Are you sure you want to delete this chunk?{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={isDeleting}
|
||||
onClick={onClose}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteChunk}
|
||||
disabled={isDeleting}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
@@ -102,9 +111,9 @@ export function DeleteChunkModal({
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/emcn'
|
||||
import {
|
||||
ChunkTableSkeleton,
|
||||
KnowledgeHeader,
|
||||
@@ -63,12 +63,8 @@ export function DocumentLoading({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled
|
||||
size='sm'
|
||||
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-muted-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
|
||||
>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
<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>
|
||||
|
||||
@@ -2,21 +2,15 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertCircle, ChevronDown, ChevronUp, Loader2, X } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
Button,
|
||||
Label,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTitle,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
|
||||
@@ -59,20 +53,16 @@ export function EditChunkModal({
|
||||
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
|
||||
const [pendingNavigation, setPendingNavigation] = useState<(() => void) | null>(null)
|
||||
|
||||
// Check if there are unsaved changes
|
||||
const hasUnsavedChanges = editedContent !== (chunk?.content || '')
|
||||
|
||||
// Update edited content when chunk changes
|
||||
useEffect(() => {
|
||||
if (chunk?.content) {
|
||||
setEditedContent(chunk.content)
|
||||
}
|
||||
}, [chunk?.id, chunk?.content])
|
||||
|
||||
// Find current chunk index in the current page
|
||||
const currentChunkIndex = chunk ? allChunks.findIndex((c) => c.id === chunk.id) : -1
|
||||
|
||||
// Calculate navigation availability
|
||||
const canNavigatePrev = currentChunkIndex > 0 || currentPage > 1
|
||||
const canNavigateNext = currentChunkIndex < allChunks.length - 1 || currentPage < totalPages
|
||||
|
||||
@@ -122,16 +112,13 @@ export function EditChunkModal({
|
||||
|
||||
if (direction === 'prev') {
|
||||
if (currentChunkIndex > 0) {
|
||||
// Navigate to previous chunk in current page
|
||||
const prevChunk = allChunks[currentChunkIndex - 1]
|
||||
onNavigateToChunk?.(prevChunk)
|
||||
} else if (currentPage > 1) {
|
||||
// Load previous page and navigate to last chunk
|
||||
await onNavigateToPage?.(currentPage - 1, 'last')
|
||||
}
|
||||
} else {
|
||||
if (currentChunkIndex < allChunks.length - 1) {
|
||||
// Navigate to next chunk in current page
|
||||
const nextChunk = allChunks[currentChunkIndex + 1]
|
||||
onNavigateToChunk?.(nextChunk)
|
||||
} else if (currentPage < totalPages) {
|
||||
@@ -181,15 +168,18 @@ export function EditChunkModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={handleCloseAttempt}>
|
||||
<DialogContent
|
||||
<Modal open={isOpen} onOpenChange={handleCloseAttempt}>
|
||||
<ModalContent
|
||||
className='flex h-[74vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
hideCloseButton
|
||||
showClose={false}
|
||||
>
|
||||
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<DialogTitle className='font-medium text-lg'>Edit Chunk</DialogTitle>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Edit Chunk
|
||||
</ModalTitle>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<div className='flex items-center gap-1'>
|
||||
@@ -201,7 +191,6 @@ export function EditChunkModal({
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => handleNavigate('prev')}
|
||||
disabled={!canNavigatePrev || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
@@ -223,7 +212,6 @@ export function EditChunkModal({
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => handleNavigate('next')}
|
||||
disabled={!canNavigateNext || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
@@ -241,125 +229,140 @@ export function EditChunkModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 p-0'
|
||||
onClick={handleCloseAttempt}
|
||||
>
|
||||
<Button variant='ghost' className='h-8 w-8 p-0' onClick={handleCloseAttempt}>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col overflow-hidden'>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto px-6'>
|
||||
<div className='flex min-h-full flex-col py-4'>
|
||||
{/* Document Info Section - Fixed at top */}
|
||||
<div className='flex-shrink-0 space-y-4'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Editing chunk #{chunk.chunkIndex} • Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'>
|
||||
<div className='flex min-h-full flex-col px-6'>
|
||||
<div className='flex flex-1 flex-col space-y-[12px] pt-0 pb-6'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex-shrink-0 space-y-[8px]'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Editing chunk #{chunk.chunkIndex} • Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill space */}
|
||||
<div className='flex min-h-0 flex-1 flex-col space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='font-medium text-[13px] text-[var(--text-primary)] dark: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'
|
||||
}
|
||||
className='min-h-0 flex-1 resize-none'
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
disabled={isSaving || isNavigating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{userPermissions.canEdit && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSaveContent}
|
||||
type='button'
|
||||
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill remaining space */}
|
||||
<div className='mt-4 flex flex-1 flex-col'>
|
||||
<Label htmlFor='content' className='mb-2 font-medium text-sm'>
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder={
|
||||
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
|
||||
}
|
||||
className='flex-1 resize-none'
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='mt-auto border-t px-6 pt-4 pb-6'>
|
||||
<div className='flex justify-between'>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={handleCloseAttempt}
|
||||
disabled={isSaving || isNavigating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{userPermissions.canEdit && (
|
||||
<Button
|
||||
onClick={handleSaveContent}
|
||||
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Unsaved Changes Alert */}
|
||||
<AlertDialog open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<ModalContent className='flex flex-col gap-0 p-0'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Unsaved Changes
|
||||
</ModalTitle>
|
||||
<p className='mt-2 text-[12px] text-[var(--text-secondary)] dark:text-[var(--text-secondary)]'>
|
||||
You have unsaved changes to this chunk content.
|
||||
{pendingNavigation
|
||||
? ' Do you want to discard your changes and navigate to the next chunk?'
|
||||
: ' Are you sure you want to discard your changes and close the editor?'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => {
|
||||
setShowUnsavedChangesAlert(false)
|
||||
setPendingNavigation(null)
|
||||
}}
|
||||
type='button'
|
||||
>
|
||||
Keep Editing
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleConfirmDiscard}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
type='button'
|
||||
className='bg-[var(--text-error)] hover:bg-[var(--text-error)] dark:bg-[var(--text-error)] dark:hover:bg-[var(--text-error)]'
|
||||
>
|
||||
Discard Changes
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { Suspense, startTransition, useCallback, useEffect, useState } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Circle, CircleOff, FileText, Plus } from 'lucide-react'
|
||||
import { useParams, useSearchParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button, Checkbox, SearchHighlight } from '@/components/ui'
|
||||
import { Checkbox, SearchHighlight } from '@/components/ui'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
CreateChunkModal,
|
||||
@@ -339,7 +339,6 @@ export function Document({
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleToggleEnabled(chunk.id)
|
||||
@@ -362,7 +361,6 @@ export function Document({
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteChunk(chunk.id)
|
||||
@@ -679,8 +677,8 @@ export function Document({
|
||||
<Button
|
||||
onClick={() => setIsCreateChunkModalOpen(true)}
|
||||
disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit}
|
||||
size='sm'
|
||||
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
variant='primary'
|
||||
className='flex items-center gap-1'
|
||||
>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
<span>Create Chunk</span>
|
||||
@@ -781,7 +779,6 @@ export function Document({
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={prevPage}
|
||||
disabled={!hasPrevPage}
|
||||
className='h-8 w-8 p-0'
|
||||
@@ -822,7 +819,6 @@ export function Document({
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={nextPage}
|
||||
disabled={!hasNextPage}
|
||||
className='h-8 w-8 p-0'
|
||||
|
||||
@@ -16,18 +16,17 @@ import {
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { SearchHighlight } from '@/components/ui/search-highlight'
|
||||
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
import {
|
||||
getDocumentIcon,
|
||||
KnowledgeHeader,
|
||||
PrimaryButton,
|
||||
SearchInput,
|
||||
} from '@/app/workspace/[workspaceId]/knowledge/components'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
@@ -155,6 +153,7 @@ export function KnowledgeBase({
|
||||
knowledgeBase,
|
||||
isLoading: isLoadingKnowledgeBase,
|
||||
error: knowledgeBaseError,
|
||||
refresh: refreshKnowledgeBase,
|
||||
} = useKnowledgeBase(id)
|
||||
const {
|
||||
documents,
|
||||
@@ -176,12 +175,10 @@ export function KnowledgeBase({
|
||||
const knowledgeBaseName = knowledgeBase?.name || passedKnowledgeBaseName || 'Knowledge Base'
|
||||
const error = knowledgeBaseError || documentsError
|
||||
|
||||
// Pagination calculations
|
||||
const totalPages = Math.ceil(pagination.total / pagination.limit)
|
||||
const hasNextPage = currentPage < totalPages
|
||||
const hasPrevPage = currentPage > 1
|
||||
|
||||
// Navigation functions
|
||||
const goToPage = useCallback(
|
||||
(page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
@@ -206,20 +203,16 @@ export function KnowledgeBase({
|
||||
const handleSort = useCallback(
|
||||
(field: DocumentSortField) => {
|
||||
if (sortBy === field) {
|
||||
// Toggle sort order if same field
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
// Set new field with default desc order
|
||||
setSortBy(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
// Reset to first page when sorting changes
|
||||
setCurrentPage(1)
|
||||
},
|
||||
[sortBy, sortOrder]
|
||||
)
|
||||
|
||||
// Helper function to render sortable header
|
||||
const renderSortableHeader = (field: DocumentSortField, label: string, className = '') => (
|
||||
<th className={`px-4 pt-2 pb-3 text-left font-medium ${className}`}>
|
||||
<button
|
||||
@@ -238,7 +231,6 @@ export function KnowledgeBase({
|
||||
</th>
|
||||
)
|
||||
|
||||
// Auto-refresh documents when there are processing documents
|
||||
useEffect(() => {
|
||||
const hasProcessingDocuments = documents.some(
|
||||
(doc) => doc.processingStatus === 'pending' || doc.processingStatus === 'processing'
|
||||
@@ -248,9 +240,7 @@ export function KnowledgeBase({
|
||||
|
||||
const refreshInterval = setInterval(async () => {
|
||||
try {
|
||||
// Only refresh if we're not in the middle of other operations
|
||||
if (!isDeleting) {
|
||||
// Check for dead processes before refreshing
|
||||
await checkForDeadProcesses()
|
||||
await refreshDocuments()
|
||||
}
|
||||
@@ -262,7 +252,6 @@ export function KnowledgeBase({
|
||||
return () => clearInterval(refreshInterval)
|
||||
}, [documents, refreshDocuments, isDeleting])
|
||||
|
||||
// Check for documents stuck in processing due to dead processes
|
||||
const checkForDeadProcesses = async () => {
|
||||
const now = new Date()
|
||||
const DEAD_PROCESS_THRESHOLD_MS = 150 * 1000 // 150 seconds (2.5 minutes)
|
||||
@@ -280,7 +269,6 @@ export function KnowledgeBase({
|
||||
|
||||
logger.warn(`Found ${staleDocuments.length} documents with dead processes`)
|
||||
|
||||
// Mark stale documents as failed via API to sync with database
|
||||
const markFailedPromises = staleDocuments.map(async (doc) => {
|
||||
try {
|
||||
const response = await fetch(`/api/knowledge/${id}/documents/${doc.id}`, {
|
||||
@@ -294,7 +282,6 @@ export function KnowledgeBase({
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If API call fails, log but don't throw to avoid stopping other recoveries
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
||||
logger.error(`Failed to mark document ${doc.id} as failed: ${errorData.error}`)
|
||||
return
|
||||
@@ -312,7 +299,6 @@ export function KnowledgeBase({
|
||||
await Promise.allSettled(markFailedPromises)
|
||||
}
|
||||
|
||||
// Calculate pagination info for display
|
||||
const totalItems = pagination?.total || 0
|
||||
|
||||
const handleToggleEnabled = async (docId: string) => {
|
||||
@@ -701,6 +687,7 @@ export function KnowledgeBase({
|
||||
options={{
|
||||
knowledgeBaseId: id,
|
||||
currentWorkspaceId: knowledgeBase?.workspaceId || null,
|
||||
onWorkspaceChange: refreshKnowledgeBase,
|
||||
onDeleteKnowledgeBase: () => setShowDeleteDialog(true),
|
||||
}}
|
||||
/>
|
||||
@@ -723,13 +710,15 @@ export function KnowledgeBase({
|
||||
{/* Add Documents Button */}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<PrimaryButton
|
||||
<Button
|
||||
onClick={handleAddDocuments}
|
||||
disabled={userPermissions.canEdit !== true}
|
||||
variant='primary'
|
||||
className='flex items-center gap-1'
|
||||
>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
Add Documents
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{userPermissions.canEdit !== true && (
|
||||
<Tooltip.Content>Write permission required to add documents</Tooltip.Content>
|
||||
@@ -1153,28 +1142,38 @@ export function KnowledgeBase({
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Knowledge Base</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Knowledge Base</ModalTitle>
|
||||
<ModalDescription>
|
||||
Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete
|
||||
the knowledge base and all {totalItems} document
|
||||
{totalItems === 1 ? '' : 's'} within it. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
{totalItems === 1 ? '' : 's'} within it.{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='outline'
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
onClick={handleDeleteKnowledgeBase}
|
||||
disabled={isDeleting}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Knowledge Base'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Upload Modal */}
|
||||
<UploadModal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Search } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/emcn'
|
||||
import {
|
||||
DocumentTableSkeleton,
|
||||
KnowledgeHeader,
|
||||
@@ -52,13 +52,9 @@ export function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoading
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* Add Documents Button - disabled state */}
|
||||
<Button
|
||||
disabled
|
||||
size='sm'
|
||||
className='flex items-center gap-1 bg-[var(--brand-primary-hex)] font-[480] text-muted-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50'
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { AlertCircle, Check, Loader2, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -149,11 +155,11 @@ export function UploadModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className='flex max-h-[95vh] flex-col overflow-hidden sm:max-w-[600px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload Documents</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Modal open={open} onOpenChange={handleClose}>
|
||||
<ModalContent className='flex max-h-[95vh] flex-col overflow-hidden sm:max-w-[600px]'>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Upload Documents</ModalTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<div className='flex-1 space-y-6 overflow-auto'>
|
||||
{/* File Upload Section */}
|
||||
@@ -253,7 +259,6 @@ export function UploadModal({
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className='h-8 w-8 p-0 text-muted-foreground hover:text-destructive'
|
||||
@@ -286,29 +291,31 @@ export function UploadModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-between border-t pt-4'>
|
||||
<div className='flex gap-3' />
|
||||
<div className='flex gap-3'>
|
||||
<Button variant='outline' onClick={handleClose} disabled={isUploading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={files.length === 0 || isUploading}
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
>
|
||||
{isUploading
|
||||
? uploadProgress.stage === 'uploading'
|
||||
? `Uploading ${uploadProgress.filesCompleted + 1}/${uploadProgress.totalFiles}...`
|
||||
: uploadProgress.stage === 'processing'
|
||||
? 'Processing...'
|
||||
: 'Uploading...'
|
||||
: `Upload ${files.length} file${files.length !== 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={handleClose}
|
||||
disabled={isUploading}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleUpload}
|
||||
disabled={files.length === 0 || isUploading}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ import { AlertCircle, Check, Loader2, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { Button, Input, Label, Modal, ModalContent, ModalTitle, Textarea } from '@/components/emcn'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
|
||||
@@ -312,34 +308,27 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent
|
||||
className='flex h-[74vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
hideCloseButton
|
||||
>
|
||||
<DialogHeader className='flex-shrink-0 border-b px-6 py-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<DialogTitle className='font-medium text-lg'>Create Knowledge Base</DialogTitle>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 p-0'
|
||||
onClick={() => handleClose(false)}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<Modal open={open} onOpenChange={handleClose}>
|
||||
<ModalContent className='flex h-[78vh] max-h-[95vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[750px]'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Create Knowledge Base
|
||||
</ModalTitle>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col overflow-hidden'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex h-full flex-col'>
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto px-6'>
|
||||
<div className='flex min-h-full flex-col py-4'>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'
|
||||
>
|
||||
<div className='px-6'>
|
||||
{/* Show upload error first, then submit error only if no upload error */}
|
||||
{uploadError && (
|
||||
<Alert variant='destructive' className='mb-6'>
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Upload Error</AlertTitle>
|
||||
<AlertDescription>{uploadError.message}</AlertDescription>
|
||||
@@ -347,16 +336,16 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
)}
|
||||
|
||||
{submitStatus && submitStatus.type === 'error' && !uploadError && (
|
||||
<Alert variant='destructive' className='mb-6'>
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{submitStatus.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Form Fields Section - Fixed at top */}
|
||||
<div className='flex-shrink-0 space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
{/* Form Fields Section */}
|
||||
<div className='space-y-[12px]'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='name'>Name *</Label>
|
||||
<Input
|
||||
id='name'
|
||||
@@ -369,7 +358,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='description'>Description</Label>
|
||||
<Textarea
|
||||
id='description'
|
||||
@@ -384,12 +373,12 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
|
||||
{/* Chunk Configuration Section */}
|
||||
<div className='space-y-4 rounded-lg border p-4'>
|
||||
<div className='space-y-[12px] rounded-lg border p-5'>
|
||||
<h3 className='font-medium text-foreground text-sm'>Chunking Configuration</h3>
|
||||
|
||||
{/* Min and Max Chunk Size Row */}
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='minChunkSize'>Min Chunk Size</Label>
|
||||
<Input
|
||||
id='minChunkSize'
|
||||
@@ -406,7 +395,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='maxChunkSize'>Max Chunk Size</Label>
|
||||
<Input
|
||||
id='maxChunkSize'
|
||||
@@ -425,7 +414,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
|
||||
{/* Overlap Size */}
|
||||
<div className='space-y-2'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='overlapSize'>Overlap Size</Label>
|
||||
<Input
|
||||
id='overlapSize'
|
||||
@@ -447,12 +436,10 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
provide more precise retrieval but may lose context.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Upload Section - Expands to fill remaining space */}
|
||||
<div className='mt-6 flex flex-1 flex-col'>
|
||||
<Label className='mb-2'>Upload Documents</Label>
|
||||
<div className='flex flex-1 flex-col'>
|
||||
{/* File Upload Section */}
|
||||
<div className='space-y-[12px]'>
|
||||
<Label>Upload Documents</Label>
|
||||
{files.length === 0 ? (
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
@@ -461,7 +448,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`relative flex flex-1 cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed py-8 text-center transition-all duration-200 ${
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed py-8 text-center transition-all duration-200 ${
|
||||
isDragging
|
||||
? 'border-purple-300 bg-purple-50 shadow-sm'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/40 hover:bg-muted/10'
|
||||
@@ -494,7 +481,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-1 flex-col space-y-2'>
|
||||
<div className='space-y-2'>
|
||||
{/* Compact drop area at top of file list */}
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
@@ -579,7 +566,6 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className='h-8 w-8 p-0 text-muted-foreground hover:text-destructive'
|
||||
@@ -593,7 +579,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
)}
|
||||
{fileError && (
|
||||
<Alert variant='destructive' className='mt-2'>
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{fileError}</AlertDescription>
|
||||
@@ -604,16 +590,21 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='mt-auto border-t px-6 pt-4 pb-6'>
|
||||
<div className='flex justify-between'>
|
||||
<Button variant='outline' onClick={() => handleClose(false)} type='button'>
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => handleClose(false)}
|
||||
type='button'
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
type='submit'
|
||||
disabled={isSubmitting || !nameValue?.trim()}
|
||||
className='bg-[var(--brand-primary-hex)] font-[480] text-primary-foreground shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50 disabled:hover:shadow-none'
|
||||
>
|
||||
{isSubmitting
|
||||
? isUploading
|
||||
@@ -629,7 +620,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,6 +144,62 @@ export const TxtIcon: React.FC<IconProps> = ({ className = 'w-6 h-6' }) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const AudioIcon: React.FC<IconProps> = ({ className = 'w-6 h-6' }) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' className={className}>
|
||||
<path
|
||||
d='M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z'
|
||||
fill='#0288D1'
|
||||
/>
|
||||
<path d='M14 2V8H20' fill='#29B6F6' />
|
||||
<path
|
||||
d='M14 2L20 8V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V4C4 2.9 4.9 2 6 2H14Z'
|
||||
stroke='#01579B'
|
||||
strokeWidth='0.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
{/* Speaker icon */}
|
||||
<path d='M8.5 10.5v3c0 .28.22.5.5.5h1.5l2 2V8l-2 2H9c-.28 0-.5.22-.5.5z' fill='white' />
|
||||
{/* Sound waves */}
|
||||
<path
|
||||
d='M14 10.5c.6.6.6 1.4 0 2M15.5 9c1.2 1.2 1.2 3.8 0 5'
|
||||
stroke='white'
|
||||
strokeWidth='0.8'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const VideoIcon: React.FC<IconProps> = ({ className = 'w-6 h-6' }) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' className={className}>
|
||||
<path
|
||||
d='M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z'
|
||||
fill='#D32F2F'
|
||||
/>
|
||||
<path d='M14 2V8H20' fill='#EF5350' />
|
||||
<path
|
||||
d='M14 2L20 8V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V4C4 2.9 4.9 2 6 2H14Z'
|
||||
stroke='#B71C1C'
|
||||
strokeWidth='0.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
{/* Video screen */}
|
||||
<rect
|
||||
x='7.5'
|
||||
y='9.5'
|
||||
width='9'
|
||||
height='6'
|
||||
rx='0.5'
|
||||
stroke='white'
|
||||
strokeWidth='0.8'
|
||||
fill='none'
|
||||
/>
|
||||
{/* Play button */}
|
||||
<path d='M10.5 11.5l3 2-3 2v-4z' fill='white' />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const DefaultFileIcon: React.FC<IconProps> = ({ className = 'w-6 h-6' }) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' className={className}>
|
||||
<path
|
||||
@@ -164,13 +220,23 @@ export const DefaultFileIcon: React.FC<IconProps> = ({ className = 'w-6 h-6' })
|
||||
</svg>
|
||||
)
|
||||
|
||||
// Helper function to get the appropriate icon component
|
||||
export function getDocumentIcon(mimeType: string, filename: string): React.FC<IconProps> {
|
||||
const extension = filename.split('.').pop()?.toLowerCase()
|
||||
|
||||
const audioExtensions = ['mp3', 'm4a', 'wav', 'webm', 'ogg', 'flac', 'aac', 'opus']
|
||||
if (mimeType.startsWith('audio/') || (extension && audioExtensions.includes(extension))) {
|
||||
return AudioIcon
|
||||
}
|
||||
|
||||
const videoExtensions = ['mp4', 'mov', 'avi', 'mkv']
|
||||
if (mimeType.startsWith('video/') || (extension && videoExtensions.includes(extension))) {
|
||||
return VideoIcon
|
||||
}
|
||||
|
||||
if (mimeType === 'application/pdf' || extension === 'pdf') {
|
||||
return PdfIcon
|
||||
}
|
||||
|
||||
if (
|
||||
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
||||
mimeType === 'application/msword' ||
|
||||
@@ -179,6 +245,7 @@ export function getDocumentIcon(mimeType: string, filename: string): React.FC<Ic
|
||||
) {
|
||||
return DocxIcon
|
||||
}
|
||||
|
||||
if (
|
||||
mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
mimeType === 'application/vnd.ms-excel' ||
|
||||
@@ -187,11 +254,14 @@ export function getDocumentIcon(mimeType: string, filename: string): React.FC<Ic
|
||||
) {
|
||||
return XlsxIcon
|
||||
}
|
||||
|
||||
if (mimeType === 'text/csv' || extension === 'csv') {
|
||||
return CsvIcon
|
||||
}
|
||||
|
||||
if (mimeType === 'text/plain' || extension === 'txt') {
|
||||
return TxtIcon
|
||||
}
|
||||
|
||||
return DefaultFileIcon
|
||||
}
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { LibraryBig, MoreHorizontal } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { WorkspaceSelector } from '@/app/workspace/[workspaceId]/knowledge/components'
|
||||
import {
|
||||
commandListClass,
|
||||
dropdownContentClass,
|
||||
filterButtonClass,
|
||||
} from '@/app/workspace/[workspaceId]/knowledge/components/shared'
|
||||
import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/shared'
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string
|
||||
@@ -46,6 +37,8 @@ interface KnowledgeHeaderProps {
|
||||
}
|
||||
|
||||
export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps) {
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={HEADER_STYLES.container}>
|
||||
<div className={HEADER_STYLES.breadcrumbs}>
|
||||
@@ -85,35 +78,29 @@ export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps)
|
||||
|
||||
{/* Actions Menu */}
|
||||
{options.onDeleteKnowledgeBase && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Popover open={isActionsPopoverOpen} onOpenChange={setIsActionsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className={filterButtonClass}
|
||||
aria-label='Knowledge base actions menu'
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align='end'
|
||||
side='bottom'
|
||||
avoidCollisions={false}
|
||||
sideOffset={4}
|
||||
className={dropdownContentClass}
|
||||
>
|
||||
<div className={`${commandListClass} py-1`}>
|
||||
<DropdownMenuItem
|
||||
onClick={options.onDeleteKnowledgeBase}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 font-[380] text-red-600 text-sm hover:bg-secondary/50 focus:bg-secondary/50 focus:text-red-600'
|
||||
>
|
||||
<Trash className='h-4 w-4' />
|
||||
Delete Knowledge Base
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' side='bottom' sideOffset={4}>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
options.onDeleteKnowledgeBase?.()
|
||||
setIsActionsPopoverOpen(false)
|
||||
}}
|
||||
className='text-red-600 hover:text-red-600 focus:text-red-600'
|
||||
>
|
||||
<Trash className='h-4 w-4' />
|
||||
<span>Delete Knowledge Base</span>
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle, Check, ChevronDown } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertTriangle, ChevronDown } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
Button,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverItem,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
commandListClass,
|
||||
dropdownContentClass,
|
||||
filterButtonClass,
|
||||
} from '@/app/workspace/[workspaceId]/knowledge/components/shared'
|
||||
import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/shared'
|
||||
import { useKnowledgeStore } from '@/stores/knowledge/store'
|
||||
|
||||
const logger = createLogger('WorkspaceSelector')
|
||||
@@ -43,6 +39,7 @@ export function WorkspaceSelector({
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||
|
||||
// Fetch available workspaces
|
||||
useEffect(() => {
|
||||
@@ -82,6 +79,7 @@ export function WorkspaceSelector({
|
||||
|
||||
try {
|
||||
setIsUpdating(true)
|
||||
setIsPopoverOpen(false)
|
||||
|
||||
const response = await fetch(`/api/knowledge/${knowledgeBaseId}`, {
|
||||
method: 'PUT',
|
||||
@@ -103,11 +101,11 @@ export function WorkspaceSelector({
|
||||
if (result.success) {
|
||||
logger.info(`Knowledge base workspace updated: ${knowledgeBaseId} -> ${workspaceId}`)
|
||||
|
||||
// Update the store immediately to reflect the change without page reload
|
||||
updateKnowledgeBase(knowledgeBaseId, { workspaceId: workspaceId || undefined })
|
||||
// Notify parent component of the change to refresh data
|
||||
await onWorkspaceChange?.(workspaceId)
|
||||
|
||||
// Notify parent component of the change
|
||||
onWorkspaceChange?.(workspaceId)
|
||||
// Update the store after refresh to ensure consistency
|
||||
updateKnowledgeBase(knowledgeBaseId, { workspaceId: workspaceId || undefined })
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to update workspace')
|
||||
}
|
||||
@@ -134,11 +132,10 @@ export function WorkspaceSelector({
|
||||
)}
|
||||
|
||||
{/* Workspace selector dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
disabled={disabled || isLoading || isUpdating}
|
||||
className={filterButtonClass}
|
||||
>
|
||||
@@ -151,53 +148,36 @@ export function WorkspaceSelector({
|
||||
</span>
|
||||
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align='end'
|
||||
side='bottom'
|
||||
avoidCollisions={false}
|
||||
sideOffset={4}
|
||||
className={dropdownContentClass}
|
||||
>
|
||||
<div className={`${commandListClass} py-1`}>
|
||||
{/* No workspace option */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleWorkspaceChange(null)}
|
||||
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align='end' side='bottom' sideOffset={4}>
|
||||
{/* No workspace option */}
|
||||
<PopoverItem
|
||||
active={!currentWorkspaceId}
|
||||
showCheck
|
||||
onClick={() => handleWorkspaceChange(null)}
|
||||
>
|
||||
<span className='text-muted-foreground'>No workspace</span>
|
||||
</PopoverItem>
|
||||
|
||||
{/* Available workspaces */}
|
||||
{workspaces.map((workspace) => (
|
||||
<PopoverItem
|
||||
key={workspace.id}
|
||||
active={currentWorkspaceId === workspace.id}
|
||||
showCheck
|
||||
onClick={() => handleWorkspaceChange(workspace.id)}
|
||||
>
|
||||
<span className='text-muted-foreground'>No workspace</span>
|
||||
{!currentWorkspaceId && <Check className='h-4 w-4 text-muted-foreground' />}
|
||||
</DropdownMenuItem>
|
||||
{workspace.name}
|
||||
</PopoverItem>
|
||||
))}
|
||||
|
||||
{/* Available workspaces */}
|
||||
{workspaces.map((workspace) => (
|
||||
<DropdownMenuItem
|
||||
key={workspace.id}
|
||||
onClick={() => handleWorkspaceChange(workspace.id)}
|
||||
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<span>{workspace.name}</span>
|
||||
<span className='text-muted-foreground text-xs capitalize'>
|
||||
{workspace.permissions}
|
||||
</span>
|
||||
</div>
|
||||
{currentWorkspaceId === workspace.id && (
|
||||
<Check className='h-4 w-4 text-muted-foreground' />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
{workspaces.length === 0 && !isLoading && (
|
||||
<DropdownMenuItem disabled className='px-3 py-2'>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
No workspaces with write access
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{workspaces.length === 0 && !isLoading && (
|
||||
<PopoverItem disabled>
|
||||
<span className='text-muted-foreground text-xs'>No workspaces with write access</span>
|
||||
</PopoverItem>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user