Compare commits

...

92 Commits

Author SHA1 Message Date
510ac2afaa Merge pull request #201 from langgenius/fix/next-security
fix: nextjs security update
2025-12-12 11:20:44 +08:00
b26ffc7f1a chore: next security 2025-12-12 11:11:22 +08:00
123d55694a Merge pull request #200 from langgenius/fix/new-react-security-issue
fix: react security issue
2025-12-12 10:03:39 +08:00
0d220f5a54 fix: react security issue 2025-12-12 09:36:49 +08:00
ee5ed029bb Merge pull request #199 from langgenius/fix/CVE-2025-55182
fix: CVE-2025-55182
2025-12-09 14:36:50 +08:00
6b0302e093 fix: CVE-2025-55182 2025-12-09 13:43:01 +08:00
55a77ea86d Merge pull request #182 from lyzno1/feature/streamdown-and-fixes
feat: Streamdown integration and UI/UX improvements
2025-09-16 10:27:39 +08:00
c4b8029702 Merge remote-tracking branch 'upstream/main' into feature/streamdown-and-fixes 2025-09-16 10:23:50 +08:00
aabdcdb3df Merge pull request #183 from lyzno1/chore/add-pnpm-lock
chore: add pnpm-lock.yaml for dependency locking
2025-09-16 10:19:38 +08:00
71a13ba418 Merge pull request #178 from zhiheng-yu/feat/staged-docker-build
feat(docker): Docker build with multi-stage
2025-09-16 10:14:07 +08:00
6a8f4e6db1 Merge pull request #125 from AllForNothing/fix/same-site
fix: add new setting item to disable/enable same site for session_id cookie
2025-09-16 10:09:59 +08:00
9b1288fc96 Merge branch 'main' into chore/add-pnpm-lock 2025-09-16 10:08:24 +08:00
518ec4fb9d Merge pull request #123 from vvatelot/i18n/french
i18n(fr): Add french translations
2025-09-16 10:07:21 +08:00
12e52b5d7e Merge pull request #181 from lyzno1/chore/eslint-v9-migration
chore: eslint v9 migration
2025-09-16 10:05:21 +08:00
238611f547 Merge pull request #143 from Laurel-rao/main
修复 iphone 手机点击聊天框放大的问题
2025-09-16 10:04:49 +08:00
d28e2c29fc chore: add pnpm-lock.yaml 2025-09-15 11:10:07 +08:00
34bb34f890 [Error] Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
error (intercept-console-error.js:57)
	getRootForUpdatedFiber (react-dom-client.development.js:3899)
	dispatchSetStateInternal (react-dom-client.development.js:8249)
	dispatchSetState (react-dom-client.development.js:8209)
	(anonymous function) (react-tooltip.min.mjs:18:14416)
2025-09-15 10:58:08 +08:00
18a4229464 Error: Route "/api/messages/[messageId]/feedbacks" used params.messageId. params should be awaited before using its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
at POST (app/api/messages/[messageId]/feedbacks/route.ts:12:11)
  10 |     rating,
  11 |   } = body
> 12 |   const { messageId } = params
     |           ^
  13 |   const { user } = getInfo(request)
  14 |   const { data } = await client.messageFeedback(messageId, rating, user)
  15 |   return NextResponse.json(data)
 POST /api/messages/36fd2d18-909b-4bb9-b46d-6e7f72a705e4/feedbacks 200 in 557ms
2025-09-15 10:57:56 +08:00
0cb8fdcf15 fix: await params in dynamic route for Next.js 15 compatibility 2025-09-15 10:57:38 +08:00
532aba026a fix: filter empty URLs in image gallery to prevent browser reload
- Add validation to filter out empty and whitespace-only image URLs
- Return null when no valid images exist
- Prevents console errors and browser reload issues from empty img src

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:57:38 +08:00
7a07e8d56b Fix auto-scroll for page-level scrolling
Replace container scrollTop logic with scrollIntoView API to support page-level scroll bars instead of internal container scrolling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:57:38 +08:00
c907432782 fix: send button ui 2025-09-15 10:56:59 +08:00
672ad29e5d Integrate Streamdown and optimize chat layout
- Replace react-markdown with Streamdown for better streaming support
- Fix input box positioning with proper sidebar offset
- Optimize scrolling behavior with main container handling scroll
- Add max-width constraint for assistant messages
- Ensure proper spacing to prevent input box overlap

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:56:59 +08:00
2d426902bd fix: max answer bubble width 2025-09-15 10:56:59 +08:00
3025356fa7 feat: question use streamdown 2025-09-15 10:56:59 +08:00
ba95a431b3 fix: assistant answer pb 2025-09-15 10:56:59 +08:00
9ff81c0306 fix: update cookies() usage for Next.js 15 compatibility
- Make getLocaleOnServer async and await cookies()
- Update LocaleLayout to be async component
- Fix react-tooltip compatibility with React 19

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:56:19 +08:00
f809b706a0 Configure husky to use lint-staged
- Update pre-commit hook to run lint-staged instead of full eslint
- Add lint-staged script to package.json
- Only lint staged files during commit for better performance

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:55:16 +08:00
05dcfcf0ca feat: migrate ESLint to v9 flat config
- Replace .eslintrc.json with eslint.config.mjs
- Simplify configuration using @antfu/eslint-config
- Add necessary ESLint plugin dependencies
- Disable overly strict style rules
- Set package.json type to module for ESM support
- Fix ESLint disable comment format

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 10:55:10 +08:00
2b1882a5e3 chore: update eslint 2025-09-15 10:55:03 +08:00
d3909927af Merge pull request #179 from Eric-Guo/main
Fix file upload (not image) in chat-flow.
2025-09-10 15:51:04 +08:00
17007b2014 Allow file upload in chat. 2025-09-08 16:15:46 +08:00
e91a1f6194 @tailwindcss/line-clamp include by default. 2025-09-08 14:56:12 +08:00
74a656fda2 Initial GPT-5-high generated cursor rules 2025-09-08 14:51:22 +08:00
8f02afed98 Bump some outdated package version 2025-09-08 14:39:47 +08:00
dc1659463e feat(docker): Docker build with multi-stage
This will significantly reduce the size of the final image used for deployment.
2025-09-04 15:11:13 +08:00
b35f0effe5 Merge pull request #169 from LeeeeeeM/fix/add-pre-answer
fix: suggestion is not rendered
2025-06-03 15:25:06 +08:00
60a33804cc fix: suggestion is not rendered 2025-05-30 09:59:27 +08:00
7495bf44a2 fix: fix option check
fix: fix option check
2025-05-27 18:26:23 +08:00
d009b00012 fix: fix option check 2025-05-27 11:33:48 +08:00
1249ea88c9 Merge pull request #157 from langgenius/fix/file-input-caused-page-crash
feat:  support file and filelist input form
2025-04-15 18:51:08 +08:00
d3482db74d chore: can set the write input 2025-04-15 18:41:13 +08:00
beda954867 chore: can use setting config 2025-04-15 18:18:46 +08:00
4ae03c2101 chore: file upload i18n 2025-04-15 17:42:55 +08:00
7216f40bee feat: add single file upload 2025-04-14 18:38:27 +08:00
e9923e8220 fix: string type 2025-04-14 16:33:06 +08:00
9a7e1be35d fix: file type not support 2025-04-14 16:17:52 +08:00
db4e78d796 修复 iphone 手机点击聊天框放大的问题 2025-03-04 14:51:29 +08:00
87c81b99dd Delete .idea/workspace.xml 2024-12-11 18:28:19 +08:00
a8ea35e5c6 fix: add new setting item to disable/enable same site property
fixes #55

Signed-off-by: 孙世军 <1083433931@qq.com>
2024-12-11 18:08:47 +08:00
41406a8596 i18n(fr): Add french translations 2024-12-10 16:09:06 +01:00
9d2d092e9e Merge pull request #95 from yusuke-ten/fix/IMainProps-type
fix IMainProps-type
2024-12-07 22:03:48 +08:00
1f5607221a Merge pull request #93 from sorphwer/fix-openStatement-typo
fix: fix openStatement typo
2024-12-07 22:03:33 +08:00
009674b231 Merge pull request #119 from langgenius/chore/add-show-error-msg
chore: add show api error msg
2024-11-25 13:17:26 +08:00
25ef02d2aa chore: add show api error msg 2024-11-25 13:15:14 +08:00
f6f65cff68 Merge pull request #118 from langgenius/docs/env-description
docs: add more description to env
2024-11-22 11:35:27 +08:00
8c6302d1fc docs: add more description to env 2024-11-22 11:33:10 +08:00
291e9a067b Merge pull request #105 from langgenius/fix/not-support-num-input
fix: not support num input
2024-09-04 18:04:16 +08:00
ac0e3e807d chore: paragrpah form type 2024-09-04 17:55:45 +08:00
b7f703852e fix: not support num input 2024-09-04 17:51:22 +08:00
ef15747e4a Merge pull request #104 from langgenius/fix/i18n-files-problem
fix: i18n problems
2024-09-03 14:54:40 +08:00
f9bd745bb0 fix: i18n problems 2024-09-03 14:52:11 +08:00
e2b37c1a9c Merge pull request #100 from bcat95/patch-5
Update i18next-config.ts
2024-09-03 14:45:52 +08:00
0f490de7ff Merge branch 'main' into patch-5 2024-09-03 14:44:35 +08:00
aaeb440210 Merge pull request #99 from bcat95/patch-1
Create app.vi.ts
2024-09-03 14:40:15 +08:00
b45262add9 Merge pull request #97 from bcat95/patch-3
Create tools.vi.ts
2024-09-03 14:39:53 +08:00
368c6b3dae Merge pull request #94 from yusuke-ten/feat/add-japanese
Add Japanese language settings to i18n
2024-09-03 14:39:28 +08:00
f6fb9c7cea Merge pull request #98 from bcat95/patch-2
Create common.vi.ts
2024-09-03 14:38:07 +08:00
69044eb8a3 Merge pull request #103 from langgenius/fix/not-show-opening-statement
fix: not show opening statement
2024-09-03 14:32:46 +08:00
cafd643c00 fix: not show opening statement 2024-09-03 14:31:05 +08:00
1c12b1dce3 Update i18next-config.ts 2024-08-18 22:43:22 +07:00
94d09ed23b Create tools.vi.ts 2024-08-18 22:41:51 +07:00
5d313f7463 Create common.vi.ts 2024-08-18 22:41:11 +07:00
97203f5ac6 Create app.vi.ts 2024-08-18 22:40:29 +07:00
349e081f1f fix IMainProps-type 2024-08-12 19:10:23 +09:00
7f24387eef Add Japanese language settings to i18n” 2024-08-12 18:44:49 +09:00
5a1c84e79f fix: fix openStatement typo 2024-08-09 21:54:41 +08:00
8d21cbc2da Merge pull request #71 from eltociear/patch-1
docs: update README.md
2024-08-07 18:29:39 +08:00
7bb19ed8ec Merge pull request #90 from langgenius/chore/hide-workflow-run-detail
chore: hide workflow run detail
2024-08-07 18:04:19 +08:00
5a85f0d427 chore: hide workflow run detail 2024-08-07 18:01:00 +08:00
96bd12af44 Merge pull request #38 from Saul-BT/feat/spanish-language
feat: add spanish language
2024-08-07 17:14:47 +08:00
484a5dc102 Merge pull request #81 from yoyocircle/main
fix: typos
2024-08-07 17:08:50 +08:00
10eb176f72 Merge pull request #84 from langgenius/fix/optional-i18n
fix: optional copywriting i18n
2024-07-31 11:56:15 +08:00
fcd6a0215d fix: optional copywriting i18n 2024-07-31 11:54:30 +08:00
f6b4b4a361 fix: typos 2024-07-17 03:59:08 +00:00
df0ae34be1 docs: update README.md
trucated -> truncated
2024-05-09 15:15:24 +09:00
884e72b4f0 Merge pull request #64 from langgenius/feat/support-workflow
Feat: support workflow
2024-04-24 10:14:30 +08:00
6933b5923b fix style of answer 2024-04-23 18:23:06 +08:00
30509d92a3 add workflow process 2024-04-23 17:57:24 +08:00
c73753138d support workflow events 2024-04-23 17:09:12 +08:00
2bd93dcbaa Merge pull request #61 from langgenius/chore/update-nextjs
chore: update Next.js version
2024-04-19 11:00:35 +08:00
f7ff288ff1 feat: add spanish language 2023-12-07 21:20:21 +01:00
276 changed files with 55119 additions and 570 deletions

View File

@ -0,0 +1,16 @@
---
description: API client usage and streaming
---
### API Client
- Use domain functions in `[service/index.ts](mdc:service/index.ts)` for app features.
- Prefer `get/post/put/del` from `[service/base.ts](mdc:service/base.ts)`; they apply base options, timeout, and error toasts.
- Set request bodies via `options.body`; it will be JSON-stringified automatically.
- Add query via `options.params` on GET.
- Downloads: set `Content-type` header to `application/octet-stream`.
### SSE Streaming
- For streaming responses, use `ssePost` from `[service/base.ts](mdc:service/base.ts)` and supply callbacks: `onData`, `onCompleted`, `onThought`, `onFile`, `onMessageEnd`, `onMessageReplace`, `onWorkflowStarted`, `onNodeStarted`, `onNodeFinished`, `onWorkflowFinished`, `onError`.
- Chat messages helper: `sendChatMessage` in `[service/index.ts](mdc:service/index.ts)` preconfigures streaming.

10
.cursor/rules/i18n.mdc Normal file
View File

@ -0,0 +1,10 @@
---
description: i18n usage and locale resolution
---
### i18n
- Server locale: `getLocaleOnServer()` reads cookie or negotiates from headers: `[i18n/server.ts](mdc:i18n/server.ts)`.
- Client locale: use `getLocaleOnClient()` / `setLocaleOnClient()` in `[i18n/client.ts](mdc:i18n/client.ts)`; uses `LOCALE_COOKIE_NAME` from config.
- Place translations in `i18n/lang/**`. Keep keys synchronized across locales.
- Render `<html lang>` using the resolved locale in `[app/layout.tsx](mdc:app/layout.tsx)`.

View File

@ -0,0 +1,21 @@
---
alwaysApply: true
---
### Project Structure Overview
- **App Router (Next.js 14)**: Entry is `app/layout.tsx` and `app/page.tsx`.
- **API Routes**: Located under `app/api/**`. Server handlers live in `route.ts` files per folder.
- **Components**: UI under `app/components/**` with feature folders (e.g., `chat`, `workflow`, `base`).
- **Services (API client)**: Client-side HTTP/SSE utilities in `[service/base.ts](mdc:service/base.ts)` and domain methods in `[service/index.ts](mdc:service/index.ts)`.
- **Config**: Global config in `[config/index.ts](mdc:config/index.ts)` and Next config in `[next.config.js](mdc:next.config.js)`.
- **i18n**: Client/server helpers in `[i18n/client.ts](mdc:i18n/client.ts)` and `[i18n/server.ts](mdc:i18n/server.ts)`, with resources in `i18n/lang/**`.
- **Styles**: Tailwind setup `[tailwind.config.js](mdc:tailwind.config.js)`, global styles under `app/styles/**`.
Key entrypoints:
- Layout: `[app/layout.tsx](mdc:app/layout.tsx)`
- Home page: `[app/page.tsx](mdc:app/page.tsx)`
- HTTP utilities: `[service/base.ts](mdc:service/base.ts)`
- API domain functions: `[service/index.ts](mdc:service/index.ts)`
- Internationalization: `[i18n/server.ts](mdc:i18n/server.ts)`, `[i18n/client.ts](mdc:i18n/client.ts)`

View File

@ -0,0 +1,20 @@
---
globs: *.ts,*.tsx
---
### TypeScript/React Conventions
- **Strict TS**: `strict: true` in `tsconfig.json`. Avoid `any`. Prefer explicit function signatures for exports.
- **Paths**: Use `@/*` alias (tsconfig `paths`) for absolute imports.
- **React 18**: Prefer function components. Use `React.memo` only for measurable perf wins.
- **Hooks**: Co-locate hooks under `hooks/**`. Keep hook names prefixed with `use`.
- **App Router**: Server components by default. Mark client components with `'use client'` when needed.
- **Styling**: Tailwind-first; SCSS only where necessary.
- **Classnames**: Use `classnames` or `tailwind-merge` for conditional classes.
- **Control Flow**: Early returns, handle edge cases first; avoid deep nesting.
### Next.js Notes
- Route handlers belong in `app/api/**/route.ts`.
- Do not import server-only modules into client components.
- Keep environment access to server files; avoid exposing secrets.

View File

@ -0,0 +1,11 @@
---
description: UI components and styling conventions
---
### UI Components
- Component folders under `app/components/**`; keep base primitives in `base/**` (buttons, icons, inputs, uploader, etc.).
- Larger features (chat, workflow) live in their own folders with `index.tsx` and submodules.
- Prefer colocated `style.module.css` or Tailwind classes. Global styles in `app/styles/**`.
- Use `app/components/base/toast` for error/display notifications.
- Avoid unnecessary client components; mark with `'use client'` only when needed (state, effects, browser APIs).

View File

@ -1,28 +0,0 @@
{
"extends": [
"@antfu",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
],
"no-console": "off",
"indent": "off",
"@typescript-eslint/indent": [
"error",
2,
{
"SwitchCase": 1,
"flatTernaryExpressions": false,
"ignoredNodes": [
"PropertyDefinition[decorators]",
"TSUnionType",
"FunctionExpression[params]:has(Identifier[decorators])"
]
}
],
"react-hooks/exhaustive-deps": "warn"
}
}

2
.gitignore vendored
View File

@ -47,5 +47,7 @@ package-lock.json
yarn.lock
.yarnrc.yml
# mcp
.serena
# pmpm
pnpm-lock.yaml

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
pnpm lint-staged

View File

@ -4,7 +4,7 @@
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[python]": {
"editor.formatOnType": true
@ -29,4 +29,4 @@
"i18n/lang",
"app/api/messages"
]
}
}

View File

@ -1,12 +1,17 @@
FROM --platform=linux/amd64 node:19-bullseye-slim
FROM node:22-alpine AS deps
WORKDIR /app
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn install
FROM deps AS builder
WORKDIR /app
COPY . .
RUN yarn build
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
EXPOSE 3000
CMD ["yarn","start"]
CMD ["node", "server.js"]

View File

@ -4,11 +4,15 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Config App
Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. Setting the following content:
```
# APP ID
# APP ID: This is the unique identifier for your app. You can find it in the app's detail page URL.
# For example, in the URL `https://cloud.dify.ai/app/xxx/workflow`, the value `xxx` is your APP ID.
NEXT_PUBLIC_APP_ID=
# APP API key
# APP API Key: This is the key used to authenticate your app's API requests.
# You can generate it on the app's "API Access" page by clicking the "API Key" button in the top-right corner.
NEXT_PUBLIC_APP_KEY=
# APP URL
# APP URL: This is the API's base URL. If you're using the Dify cloud service, set it to: https://api.dify.ai/v1.
NEXT_PUBLIC_API_URL=
```
@ -68,7 +72,7 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
## Deploy on Vercel
> ⚠️ If you are using [Vercel Hobby](https://vercel.com/pricing), your message will be trucated due to the limitation of vercel.
> ⚠️ If you are using [Vercel Hobby](https://vercel.com/pricing), your message will be truncated due to the limitation of vercel.
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest) {

View File

@ -1,16 +1,16 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest, { params }: {
params: { conversationId: string }
params: Promise<{ conversationId: string }>
}) {
const body = await request.json()
const {
auto_generate,
name,
} = body
const { conversationId } = params
const { conversationId } = await params
const { user } = getInfo(request)
// auto generate name

View File

@ -1,6 +1,4 @@
export const dynamic = 'force-dynamic'
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'
@ -11,7 +9,11 @@ export async function GET(request: NextRequest) {
return NextResponse.json(data, {
headers: setSession(sessionId),
})
} catch (error) {
return NextResponse.json([]);
}
catch (error: any) {
return NextResponse.json({
data: [],
error: error.message,
})
}
}

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest) {

View File

@ -1,15 +1,15 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest, { params }: {
params: { messageId: string }
params: Promise<{ messageId: string }>
}) {
const body = await request.json()
const {
rating,
} = body
const { messageId } = params
const { messageId } = await params
const { user } = getInfo(request)
const { data } = await client.messageFeedback(messageId, rating, user)
return NextResponse.json(data)

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'

View File

@ -1,7 +1,7 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { ChatClient } from 'dify-client'
import { v4 } from 'uuid'
import { API_KEY, API_URL, APP_ID } from '@/config'
import { API_KEY, API_URL, APP_ID, APP_INFO } from '@/config'
const userPrefix = `user_${APP_ID}:`
@ -15,6 +15,9 @@ export const getInfo = (request: NextRequest) => {
}
export const setSession = (sessionId: string) => {
if (APP_INFO.disable_session_same_site)
{ return { 'Set-Cookie': `session_id=${sessionId}; SameSite=None; Secure` } }
return { 'Set-Cookie': `session_id=${sessionId}` }
}

View File

@ -3,26 +3,25 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type IAppUnavailableProps = {
isUnknwonReason: boolean
interface IAppUnavailableProps {
isUnknownReason: boolean
errMessage?: string
}
const AppUnavailable: FC<IAppUnavailableProps> = ({
isUnknwonReason,
isUnknownReason,
errMessage,
}) => {
const { t } = useTranslation()
let message = errMessage
if (!errMessage)
message = (isUnknwonReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string
if (!errMessage) { message = (isUnknownReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string }
return (
<div className='flex items-center justify-center w-screen h-screen'>
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
style={{
borderRight: '1px solid rgba(0,0,0,.3)',
}}>{(errMessage || isUnknwonReason) ? 500 : 404}</h1>
}}>{(errMessage || isUnknownReason) ? 500 : 404}</h1>
<div className='text-sm'>{message}</div>
</div>
)

View File

@ -0,0 +1,45 @@
@tailwind components;
@layer components {
.action-btn {
@apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
}
.action-btn-hover {
@apply bg-state-base-hover
}
.action-btn-disabled {
@apply cursor-not-allowed
}
.action-btn-xl {
@apply p-2 w-9 h-9 rounded-lg
}
.action-btn-l {
@apply p-1.5 w-8 h-8 rounded-lg
}
/* m is for the regular button */
.action-btn-m {
@apply p-0.5 w-6 h-6 rounded-lg
}
.action-btn-xs {
@apply p-0 w-4 h-4 rounded
}
.action-btn.action-btn-active {
@apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt
}
.action-btn.action-btn-disabled {
@apply text-text-disabled
}
.action-btn.action-btn-destructive {
@apply text-text-destructive bg-state-destructive-hover
}
}

View File

@ -0,0 +1,73 @@
import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from '@/utils/classnames'
enum ActionButtonState {
Destructive = 'destructive',
Active = 'active',
Disabled = 'disabled',
Default = '',
Hover = 'hover',
}
const actionButtonVariants = cva(
'action-btn',
{
variants: {
size: {
xs: 'action-btn-xs',
m: 'action-btn-m',
l: 'action-btn-l',
xl: 'action-btn-xl',
},
},
defaultVariants: {
size: 'm',
},
},
)
export type ActionButtonProps = {
size?: 'xs' | 's' | 'm' | 'l' | 'xl'
state?: ActionButtonState
styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof actionButtonVariants>
function getActionButtonState(state: ActionButtonState) {
switch (state) {
case ActionButtonState.Destructive:
return 'action-btn-destructive'
case ActionButtonState.Active:
return 'action-btn-active'
case ActionButtonState.Disabled:
return 'action-btn-disabled'
case ActionButtonState.Hover:
return 'action-btn-hover'
default:
return ''
}
}
const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
({ className, size, state = ActionButtonState.Default, styleCss, children, ...props }, ref) => {
return (
<button
type='button'
className={classNames(
actionButtonVariants({ className, size }),
getActionButtonState(state),
)}
ref={ref}
style={styleCss}
{...props}
>
{children}
</button>
)
},
)
ActionButton.displayName = 'ActionButton'
export default ActionButton
export { ActionButton, ActionButtonState, actionButtonVariants }

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import classNames from 'classnames'
import style from './style.module.css'
export type AppIconProps = {
export interface AppIconProps {
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large'
rounded?: boolean
icon?: string

View File

@ -1,7 +1,7 @@
import { forwardRef, useEffect, useRef } from 'react'
import cn from 'classnames'
type IProps = {
interface IProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
@ -36,19 +36,16 @@ const AutoHeightTextarea = forwardRef(
let hasFocus = false
const runId = setInterval(() => {
hasFocus = doFocus()
if (hasFocus)
clearInterval(runId)
if (hasFocus) { clearInterval(runId) }
}, 100)
}
}
useEffect(() => {
if (autoFocus)
focus()
if (autoFocus) { focus() }
}, [])
useEffect(() => {
if (controlFocus)
focus()
if (controlFocus) { focus() }
}, [controlFocus])
return (

View File

@ -2,7 +2,7 @@ import type { FC, MouseEventHandler } from 'react'
import React from 'react'
import Spinner from '@/app/components/base/spinner'
export type IButtonProps = {
export interface IButtonProps {
type?: string
className?: string
disabled?: boolean
@ -21,6 +21,9 @@ const Button: FC<IButtonProps> = ({
}) => {
let style = 'cursor-pointer'
switch (type) {
case 'link':
style = disabled ? 'border-solid border border-gray-200 bg-gray-200 cursor-not-allowed text-gray-800' : 'border-solid border border-gray-200 cursor-pointer text-blue-600 bg-white hover:shadow-sm hover:border-gray-300'
break
case 'primary':
style = (disabled || loading) ? 'bg-primary-600/75 cursor-not-allowed text-white' : 'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm'
break

View File

@ -0,0 +1,16 @@
import { SupportUploadFileTypes } from './types'
// fallback for file size limit of dify_config
export const IMG_SIZE_LIMIT = 10 * 1024 * 1024
export const FILE_SIZE_LIMIT = 15 * 1024 * 1024
export const AUDIO_SIZE_LIMIT = 50 * 1024 * 1024
export const VIDEO_SIZE_LIMIT = 100 * 1024 * 1024
export const MAX_FILE_UPLOAD_LIMIT = 10
export const FILE_URL_REGEX = /^(https?|ftp):\/\//
export const FILE_EXTS: Record<string, string[]> = {
[SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'],
[SupportUploadFileTypes.document]: ['TXT', 'MD', 'MDX', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'],
[SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'AMR', 'MPGA'],
[SupportUploadFileTypes.video]: ['MP4', 'MOV', 'MPEG', 'WEBM'],
}

View File

@ -0,0 +1,129 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiUploadCloud2Line } from '@remixicon/react'
import FileInput from '../file-input'
import { useFile } from '../hooks'
import { useStore } from '../store'
import { FILE_URL_REGEX } from '../constants'
import type { FileUpload } from '../types'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
interface FileFromLinkOrLocalProps {
showFromLink?: boolean
showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode
fileConfig: FileUpload
}
const FileFromLinkOrLocal = ({
showFromLink = true,
showFromLocal = true,
trigger,
fileConfig,
}: FileFromLinkOrLocalProps) => {
const { t } = useTranslation()
const files = useStore(s => s.files)
const [open, setOpen] = useState(false)
const [url, setUrl] = useState('')
const [showError, setShowError] = useState(false)
const { handleLoadFileFromLink } = useFile(fileConfig)
const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits
const handleSaveUrl = () => {
if (!url) { return }
if (!FILE_URL_REGEX.test(url)) {
setShowError(true)
return
}
handleLoadFileFromLink(url)
setUrl('')
}
return (
<PortalToFollowElem
placement='top'
offset={4}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} asChild>
{trigger(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1001]'>
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg'>
{
showFromLink && (
<>
<div className={cn(
'flex h-8 items-center rounded-lg border border-components-input-border-active bg-components-input-bg-active p-1 shadow-xs',
showError && 'border-components-input-border-destructive',
)}>
<input
className='system-sm-regular mr-0.5 block grow appearance-none bg-transparent px-1 outline-none'
placeholder={t('common.fileUploader.pasteFileLinkInputPlaceholder') || ''}
value={url}
onChange={(e) => {
setShowError(false)
setUrl(e.target.value.trim())
}}
disabled={disabled}
/>
<Button
className='shrink-0'
// size='small'
// variant='primary'
type='primary'
disabled={!url || disabled}
onClick={handleSaveUrl}
>
{t('common.operation.ok')}
</Button>
</div>
{
showError && (
<div className='body-xs-regular mt-0.5 text-text-destructive'>
{t('common.fileUploader.pasteFileLinkInvalid')}
</div>
)
}
</>
)
}
{
showFromLink && showFromLocal && (
<div className='system-2xs-medium-uppercase flex h-7 items-center p-2 text-text-quaternary'>
<div className='mr-2 h-[1px] w-[93px] bg-gradient-to-l from-[rgba(16,24,40,0.08)]' />
OR
<div className='ml-2 h-[1px] w-[93px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]' />
</div>
)
}
{
showFromLocal && (
<Button
className='relative w-full'
// variant='secondary-accent'
disabled={disabled}
>
<RiUploadCloud2Line className='mr-1 h-4 w-4' />
{t('common.fileUploader.uploadFromComputer')}
<FileInput fileConfig={fileConfig} />
</Button>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(FileFromLinkOrLocal)

View File

@ -0,0 +1,32 @@
import cn from '@/utils/classnames'
interface FileImageRenderProps {
imageUrl: string
className?: string
alt?: string
onLoad?: () => void
onError?: () => void
showDownloadAction?: boolean
}
const FileImageRender = ({
imageUrl,
className,
alt,
onLoad,
onError,
showDownloadAction,
}: FileImageRenderProps) => {
return (
<div className={cn('border-[2px] border-effects-image-frame shadow-xs', className)}>
<img
className={cn('h-full w-full object-cover', showDownloadAction && 'cursor-pointer')}
alt={alt || 'Preview'}
onLoad={onLoad}
onError={onError}
src={imageUrl}
/>
</div>
)
}
export default FileImageRender

View File

@ -0,0 +1,48 @@
import { useFile } from './hooks'
import { useStore } from './store'
import type { FileUpload } from './types'
import { FILE_EXTS } from './constants'
import { SupportUploadFileTypes } from './types'
interface FileInputProps {
fileConfig: FileUpload
}
const FileInput = ({
fileConfig,
}: FileInputProps) => {
const files = useStore(s => s.files)
const { handleLocalFileUpload } = useFile(fileConfig)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetFiles = e.target.files
if (targetFiles) {
if (fileConfig.number_limits) {
for (let i = 0; i < targetFiles.length; i++) {
if (i + 1 + files.length <= fileConfig.number_limits) { handleLocalFileUpload(targetFiles[i]) }
}
}
else {
handleLocalFileUpload(targetFiles[0])
}
}
}
const allowedFileTypes = fileConfig.allowed_file_types
const isCustom = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
const exts = isCustom ? (fileConfig.allowed_file_extensions || []) : (allowedFileTypes?.map(type => FILE_EXTS[type]) || []).flat().map(item => `.${item}`)
const accept = exts.join(',')
return (
<input
className='absolute inset-0 block w-full cursor-pointer text-[0] opacity-0 disabled:cursor-not-allowed'
onClick={e => ((e.target as HTMLInputElement).value = '')}
type='file'
onChange={handleChange}
accept={accept}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig?.number_limits)}
multiple={!!fileConfig.number_limits && fileConfig.number_limits > 1}
/>
)
}
export default FileInput

View File

@ -0,0 +1,154 @@
import {
memo,
useState,
} from 'react'
import {
RiDeleteBinLine,
RiDownloadLine,
RiEyeLine,
} from '@remixicon/react'
import FileTypeIcon from './file-type-icon'
import FileImageRender from './file-image-render'
import type { FileEntity } from './types'
import {
downloadFile,
fileIsUploaded,
getFileAppearanceType,
getFileExtension,
} from './utils'
import { SupportUploadFileTypes } from './types'
import ActionButton from '@/app/components/base/action-button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames'
import ReplayLine from '@/app/components/base/icons/other/ReplayLine'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
interface FileInAttachmentItemProps {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
canPreview?: boolean
}
const FileInAttachmentItem = ({
file,
showDeleteAction,
showDownloadAction = true,
onRemove,
onReUpload,
canPreview,
}: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return (
<>
<div className={cn(
'flex h-12 items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pr-3 shadow-xs',
progress === -1 && 'border-state-destructive-border bg-state-destructive-hover',
)}>
<div className='flex h-12 w-12 items-center justify-center'>
{
isImageFile && (
<FileImageRender
className='h-8 w-8'
imageUrl={base64Url || url || ''}
/>
)
}
{
!isImageFile && (
<FileTypeIcon
type={getFileAppearanceType(name, type)}
size='lg'
/>
)
}
</div>
<div className='mr-1 w-0 grow'>
<div
className='system-xs-medium mb-0.5 flex items-center truncate text-text-secondary'
title={file.name}
>
<div className='truncate'>{name}</div>
</div>
<div className='system-2xs-medium-uppercase flex items-center text-text-tertiary'>
{
ext && (
<span>{ext.toLowerCase()}</span>
)
}
{
ext && (
<span className='system-2xs-medium mx-1'></span>
)
}
{
!!file.size && (
<span>{formatFileSize(file.size)}</span>
)
}
</div>
</div>
<div className='flex shrink-0 items-center'>
{
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
className='mr-2.5'
percentage={progress}
/>
)
}
{
progress === -1 && (
<ActionButton
className='mr-1'
onClick={() => onReUpload?.(id)}
>
<ReplayLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
)
}
{
showDeleteAction && (
<ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className='h-4 w-4' />
</ActionButton>
)
}
{
canPreview && isImageFile && (
<ActionButton className='mr-1' onClick={() => setImagePreviewUrl(url || '')}>
<RiEyeLine className='h-4 w-4' />
</ActionButton>
)
}
{
showDownloadAction && (
<ActionButton onClick={(e) => {
e.stopPropagation()
downloadFile(url || base64Url || '', name)
}}>
<RiDownloadLine className='h-4 w-4' />
</ActionButton>
)
}
</div>
</div>
{
imagePreviewUrl && canPreview && (
<ImagePreview
title={name}
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
/>
)
}
</>
)
}
export default memo(FileInAttachmentItem)

View File

@ -0,0 +1,91 @@
import { memo } from 'react'
import {
RiFile3Fill,
RiFileCodeFill,
RiFileExcelFill,
RiFileGifFill,
RiFileImageFill,
RiFileMusicFill,
RiFilePdf2Fill,
RiFilePpt2Fill,
RiFileTextFill,
RiFileVideoFill,
RiFileWordFill,
RiMarkdownFill,
} from '@remixicon/react'
import { FileAppearanceTypeEnum } from './types'
import type { FileAppearanceType } from './types'
import cn from '@/utils/classnames'
const FILE_TYPE_ICON_MAP = {
[FileAppearanceTypeEnum.pdf]: {
component: RiFilePdf2Fill,
color: 'text-[#EA3434]',
},
[FileAppearanceTypeEnum.image]: {
component: RiFileImageFill,
color: 'text-[#00B2EA]',
},
[FileAppearanceTypeEnum.video]: {
component: RiFileVideoFill,
color: 'text-[#844FDA]',
},
[FileAppearanceTypeEnum.audio]: {
component: RiFileMusicFill,
color: 'text-[#FF3093]',
},
[FileAppearanceTypeEnum.document]: {
component: RiFileTextFill,
color: 'text-[#6F8BB5]',
},
[FileAppearanceTypeEnum.code]: {
component: RiFileCodeFill,
color: 'text-[#BCC0D1]',
},
[FileAppearanceTypeEnum.markdown]: {
component: RiMarkdownFill,
color: 'text-[#309BEC]',
},
[FileAppearanceTypeEnum.custom]: {
component: RiFile3Fill,
color: 'text-[#BCC0D1]',
},
[FileAppearanceTypeEnum.excel]: {
component: RiFileExcelFill,
color: 'text-[#01AC49]',
},
[FileAppearanceTypeEnum.word]: {
component: RiFileWordFill,
color: 'text-[#2684FF]',
},
[FileAppearanceTypeEnum.ppt]: {
component: RiFilePpt2Fill,
color: 'text-[#FF650F]',
},
[FileAppearanceTypeEnum.gif]: {
component: RiFileGifFill,
color: 'text-[#00B2EA]',
},
}
interface FileTypeIconProps {
type: FileAppearanceType
size?: 'sm' | 'lg' | 'md'
className?: string
}
const SizeMap = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
}
const FileTypeIcon = ({
type,
size = 'sm',
className,
}: FileTypeIconProps) => {
const Icon = FILE_TYPE_ICON_MAP[type]?.component || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].component
const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].color
return <Icon className={cn('shrink-0', SizeMap[size], color, className)} />
}
export default memo(FileTypeIcon)

View File

@ -0,0 +1,361 @@
import type { ClipboardEvent } from 'react'
import {
useCallback,
useState,
} from 'react'
import { useParams } from 'next/navigation'
import produce from 'immer'
import { v4 as uuid4 } from 'uuid'
import { useTranslation } from 'react-i18next'
import { noop } from 'lodash-es'
import type { FileEntity, FileUpload, FileUploadConfigResponse } from './types'
import { useFileStore } from './store'
import {
fileUpload,
getSupportFileType,
isAllowedFileExtension,
} from './utils'
import {
AUDIO_SIZE_LIMIT,
FILE_SIZE_LIMIT,
IMG_SIZE_LIMIT,
MAX_FILE_UPLOAD_LIMIT,
VIDEO_SIZE_LIMIT,
} from './constants'
import { SupportUploadFileTypes } from './types'
import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { formatFileSize } from '@/utils/format'
const uploadRemoteFileInfo = () => {
console.log('TODO')
}
export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => {
const imgSizeLimit = Number(fileUploadConfig?.image_file_size_limit) * 1024 * 1024 || IMG_SIZE_LIMIT
const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT
const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT
const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT
const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT
return {
imgSizeLimit,
docSizeLimit,
audioSizeLimit,
videoSizeLimit,
maxFileUploadLimit,
}
}
export const useFile = (fileConfig: FileUpload) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const fileStore = useFileStore()
const params = useParams()
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig)
const checkSizeLimit = useCallback((fileType: string, fileSize: number) => {
switch (fileType) {
case SupportUploadFileTypes.image: {
if (fileSize > imgSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.image,
size: formatFileSize(imgSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.document: {
if (fileSize > docSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.document,
size: formatFileSize(docSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.audio: {
if (fileSize > audioSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.audio,
size: formatFileSize(audioSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.video: {
if (fileSize > videoSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.video,
size: formatFileSize(videoSizeLimit),
}),
})
return false
}
return true
}
case SupportUploadFileTypes.custom: {
if (fileSize > docSizeLimit) {
notify({
type: 'error',
message: t('common.fileUploader.uploadFromComputerLimit', {
type: SupportUploadFileTypes.document,
size: formatFileSize(docSizeLimit),
}),
})
return false
}
return true
}
default: {
return true
}
}
}, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit])
const handleAddFile = useCallback((newFile: FileEntity) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = produce(files, (draft) => {
draft.push(newFile)
})
setFiles(newFiles)
}, [fileStore])
const handleUpdateFile = useCallback((newFile: FileEntity) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = produce(files, (draft) => {
const index = draft.findIndex(file => file.id === newFile.id)
if (index > -1) { draft[index] = newFile }
})
setFiles(newFiles)
}, [fileStore])
const handleRemoveFile = useCallback((fileId: string) => {
const {
files,
setFiles,
} = fileStore.getState()
const newFiles = files.filter(file => file.id !== fileId)
setFiles(newFiles)
}, [fileStore])
const handleReUploadFile = useCallback((fileId: string) => {
const {
files,
setFiles,
} = fileStore.getState()
const index = files.findIndex(file => file.id === fileId)
if (index > -1) {
const uploadingFile = files[index]
const newFiles = produce(files, (draft) => {
draft[index].progress = 0
})
setFiles(newFiles)
fileUpload({
file: uploadingFile.originalFile!,
onProgressCallback: (progress) => {
handleUpdateFile({ ...uploadingFile, progress })
},
onSuccessCallback: (res) => {
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleUpdateFile({ ...uploadingFile, progress: -1 })
},
})
}
}, [fileStore, notify, t, handleUpdateFile])
const startProgressTimer = useCallback((fileId: string) => {
const timer = setInterval(() => {
const files = fileStore.getState().files
const file = files.find(file => file.id === fileId)
if (file && file.progress < 80 && file.progress >= 0) { handleUpdateFile({ ...file, progress: file.progress + 20 }) }
else { clearTimeout(timer) }
}, 200)
}, [fileStore, handleUpdateFile])
const handleLoadFileFromLink = useCallback((url: string) => {
const allowedFileTypes = fileConfig.allowed_file_types
const uploadingFile = {
id: uuid4(),
name: url,
type: '',
size: 0,
progress: 0,
transferMethod: TransferMethod.remote_url,
supportFileType: '',
url,
isRemote: true,
}
handleAddFile(uploadingFile)
startProgressTimer(uploadingFile.id)
uploadRemoteFileInfo(url, !!params.token).then((res) => {
const newFile = {
...uploadingFile,
type: res.mime_type,
size: res.size,
progress: 100,
supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
uploadedId: res.id,
url: res.url,
}
if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
handleRemoveFile(uploadingFile.id)
}
if (!checkSizeLimit(newFile.supportFileType, newFile.size)) { handleRemoveFile(uploadingFile.id) }
else { handleUpdateFile(newFile) }
}).catch(() => {
notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
handleRemoveFile(uploadingFile.id)
})
}, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer, params.token])
const handleLoadFileFromLinkSuccess = useCallback(noop, [])
const handleLoadFileFromLinkError = useCallback(noop, [])
const handleClearFiles = useCallback(() => {
const {
setFiles,
} = fileStore.getState()
setFiles([])
}, [fileStore])
const handleLocalFileUpload = useCallback((file: File) => {
if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
return
}
const allowedFileTypes = fileConfig.allowed_file_types
const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom))
if (!checkSizeLimit(fileType, file.size)) { return }
const reader = new FileReader()
const isImage = file.type.startsWith('image')
reader.addEventListener(
'load',
() => {
const uploadingFile = {
id: uuid4(),
name: file.name,
type: file.type,
size: file.size,
progress: 0,
transferMethod: TransferMethod.local_file,
supportFileType: getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
originalFile: file,
base64Url: isImage ? reader.result as string : '',
}
handleAddFile(uploadingFile)
fileUpload({
file: uploadingFile.originalFile,
onProgressCallback: (progress) => {
handleUpdateFile({ ...uploadingFile, progress })
},
onSuccessCallback: (res) => {
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
handleUpdateFile({ ...uploadingFile, progress: -1 })
},
})
},
false,
)
reader.addEventListener(
'error',
() => {
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') })
},
false,
)
reader.readAsDataURL(file)
}, [checkSizeLimit, notify, t, handleAddFile, handleUpdateFile, params.token, fileConfig?.allowed_file_types, fileConfig?.allowed_file_extensions])
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
const file = e.clipboardData?.files[0]
const text = e.clipboardData?.getData('text/plain')
if (file && !text) {
e.preventDefault()
handleLocalFileUpload(file)
}
}, [handleLocalFileUpload])
const [isDragActive, setIsDragActive] = useState(false)
const handleDragFileEnter = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(true)
}, [])
const handleDragFileOver = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
}, [])
const handleDragFileLeave = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(false)
}, [])
const handleDropFile = useCallback((e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragActive(false)
const file = e.dataTransfer.files[0]
if (file) { handleLocalFileUpload(file) }
}, [handleLocalFileUpload])
return {
handleAddFile,
handleUpdateFile,
handleRemoveFile,
handleReUploadFile,
handleLoadFileFromLink,
handleLoadFileFromLinkSuccess,
handleLoadFileFromLinkError,
handleClearFiles,
handleLocalFileUpload,
handleClipboardPasteFile,
isDragActive,
handleDragFileEnter,
handleDragFileOver,
handleDragFileLeave,
handleDropFile,
}
}

View File

@ -0,0 +1,131 @@
import {
useCallback,
} from 'react'
import {
RiLink,
RiUploadCloud2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useFile } from './hooks'
import type { FileEntity, FileUpload } from './types'
import FileFromLinkOrLocal from './file-from-link-or-local'
import {
FileContextProvider,
useStore,
} from './store'
import FileInput from './file-input'
import FileItem from './file-item'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import { TransferMethod } from '@/types/app'
interface Option {
value: string
label: string
icon: JSX.Element
}
interface FileUploaderInAttachmentProps {
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
fileConfig,
}: FileUploaderInAttachmentProps) => {
const { t } = useTranslation()
const files = useStore(s => s.files)
const {
handleRemoveFile,
handleReUploadFile,
} = useFile(fileConfig)
const options = [
{
value: TransferMethod.local_file,
label: t('common.fileUploader.uploadFromComputer'),
icon: <RiUploadCloud2Line className='h-4 w-4' />,
},
{
value: TransferMethod.remote_url,
label: t('common.fileUploader.pasteFileLink'),
icon: <RiLink className='h-4 w-4' />,
},
]
const renderButton = useCallback((option: Option, open?: boolean) => {
return (
<Button
key={option.value}
// variant='tertiary'
className={cn('relative grow', open && 'bg-components-button-tertiary-bg-hover')}
disabled={!!(fileConfig.number_limits && files.length >= fileConfig.number_limits)}
>
{option.icon}
<span className='ml-1'>{option.label}</span>
{
option.value === TransferMethod.local_file && (
<FileInput fileConfig={fileConfig} />
)
}
</Button>
)
}, [fileConfig, files.length])
const renderTrigger = useCallback((option: Option) => {
return (open: boolean) => renderButton(option, open)
}, [renderButton])
const renderOption = useCallback((option: Option) => {
if (option.value === TransferMethod.local_file && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.local_file)) { return renderButton(option) }
if (option.value === TransferMethod.remote_url && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)) {
return (
<FileFromLinkOrLocal
key={option.value}
showFromLocal={false}
trigger={renderTrigger(option)}
fileConfig={fileConfig}
/>
)
}
}, [renderButton, renderTrigger, fileConfig])
return (
<div>
<div className='flex items-center space-x-1'>
{options.map(renderOption)}
</div>
<div className='mt-1 space-y-1'>
{
files.map(file => (
<FileItem
key={file.id}
file={file}
showDeleteAction
showDownloadAction={false}
onRemove={() => handleRemoveFile(file.id)}
onReUpload={() => handleReUploadFile(file.id)}
/>
))
}
</div>
</div>
)
}
interface FileUploaderInAttachmentWrapperProps {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void
fileConfig: FileUpload
}
const FileUploaderInAttachmentWrapper = ({
value,
onChange,
fileConfig,
}: FileUploaderInAttachmentWrapperProps) => {
return (
<FileContextProvider
value={value}
onChange={onChange}
>
<FileUploaderInAttachment fileConfig={fileConfig} />
</FileContextProvider>
)
}
export default FileUploaderInAttachmentWrapper

View File

@ -0,0 +1,65 @@
import {
createContext,
useContext,
useRef,
} from 'react'
import {
create,
useStore as useZustandStore,
} from 'zustand'
import type {
FileEntity,
} from './types'
interface Shape {
files: FileEntity[]
setFiles: (files: FileEntity[]) => void
}
export const createFileStore = (
value: FileEntity[] = [],
onChange?: (files: FileEntity[]) => void,
) => {
return create<Shape>(set => ({
files: value ? [...value] : [],
setFiles: (files) => {
set({ files })
onChange?.(files)
},
}))
}
type FileStore = ReturnType<typeof createFileStore>
export const FileContext = createContext<FileStore | null>(null)
export function useStore<T>(selector: (state: Shape) => T): T {
const store = useContext(FileContext)
if (!store) { throw new Error('Missing FileContext.Provider in the tree') }
return useZustandStore(store, selector)
}
export const useFileStore = () => {
return useContext(FileContext)!
}
interface FileProviderProps {
children: React.ReactNode
value?: FileEntity[]
onChange?: (files: FileEntity[]) => void
}
export const FileContextProvider = ({
children,
value,
onChange,
}: FileProviderProps) => {
const storeRef = useRef<FileStore | undefined>(undefined)
if (!storeRef.current) { storeRef.current = createFileStore(value, onChange) }
return (
<FileContext.Provider value={storeRef.current}>
{children}
</FileContext.Provider>
)
}

View File

@ -0,0 +1,83 @@
import type { TransferMethod } from '@/types/app'
export enum FileAppearanceTypeEnum {
image = 'image',
video = 'video',
audio = 'audio',
document = 'document',
code = 'code',
pdf = 'pdf',
markdown = 'markdown',
excel = 'excel',
word = 'word',
ppt = 'ppt',
gif = 'gif',
custom = 'custom',
}
export type FileAppearanceType = keyof typeof FileAppearanceTypeEnum
export interface FileEntity {
id: string
name: string
size: number
type: string
progress: number
transferMethod: TransferMethod
supportFileType: string
originalFile?: File
uploadedId?: string
base64Url?: string
url?: string
isRemote?: boolean
}
export interface EnabledOrDisabled {
enabled?: boolean
}
export enum Resolution {
low = 'low',
high = 'high',
}
export interface FileUploadConfigResponse {
batch_count_limit: number
image_file_size_limit?: number | string // default is 10MB
file_size_limit: number // default is 15MB
audio_file_size_limit?: number // default is 50MB
video_file_size_limit?: number // default is 100MB
workflow_file_upload_limit?: number // default is 10
}
export type FileUpload = {
image?: EnabledOrDisabled & {
detail?: Resolution
number_limits?: number
transfer_methods?: TransferMethod[]
}
allowed_file_types?: string[]
allowed_file_extensions?: string[]
allowed_file_upload_methods?: TransferMethod[]
number_limits?: number
fileUploadConfig?: FileUploadConfigResponse
} & EnabledOrDisabled
export enum SupportUploadFileTypes {
image = 'image',
document = 'document',
audio = 'audio',
video = 'video',
custom = 'custom',
}
export interface FileResponse {
related_id: string
extension: string
filename: string
size: number
mime_type: string
transfer_method: TransferMethod
type: string
url: string
}

View File

@ -0,0 +1,174 @@
import mime from 'mime'
import { FileAppearanceTypeEnum, SupportUploadFileTypes } from './types'
import type { FileEntity, FileResponse } from './types'
import { FILE_EXTS } from './constants'
import { upload } from '@/service/base'
import { TransferMethod } from '@/types/app'
interface FileUploadParams {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void
onErrorCallback: () => void
}
type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void
export const fileUpload: FileUpload = ({
file,
onProgressCallback,
onSuccessCallback,
onErrorCallback,
}) => {
const formData = new FormData()
formData.append('file', file)
const onProgress = (e: ProgressEvent) => {
if (e.lengthComputable) {
const percent = Math.floor(e.loaded / e.total * 100)
onProgressCallback(percent)
}
}
upload({
xhr: new XMLHttpRequest(),
data: formData,
onprogress: onProgress,
})
.then((res: { id: string }) => {
onSuccessCallback(res)
})
.catch(() => {
onErrorCallback()
})
}
export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
let extension = ''
if (fileMimetype) { extension = mime.getExtension(fileMimetype) || '' }
if (fileName && !extension) {
const fileNamePair = fileName.split('.')
const fileNamePairLength = fileNamePair.length
if (fileNamePairLength > 1) { extension = fileNamePair[fileNamePairLength - 1] }
else { extension = '' }
}
if (isRemote) { extension = '' }
return extension
}
export const getFileAppearanceType = (fileName: string, fileMimetype: string) => {
const extension = getFileExtension(fileName, fileMimetype)
if (extension === 'gif') { return FileAppearanceTypeEnum.gif }
if (FILE_EXTS.image.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.image }
if (FILE_EXTS.video.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.video }
if (FILE_EXTS.audio.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.audio }
if (extension === 'html') { return FileAppearanceTypeEnum.code }
if (extension === 'pdf') { return FileAppearanceTypeEnum.pdf }
if (extension === 'md' || extension === 'markdown' || extension === 'mdx') { return FileAppearanceTypeEnum.markdown }
if (extension === 'xlsx' || extension === 'xls') { return FileAppearanceTypeEnum.excel }
if (extension === 'docx' || extension === 'doc') { return FileAppearanceTypeEnum.word }
if (extension === 'pptx' || extension === 'ppt') { return FileAppearanceTypeEnum.ppt }
if (FILE_EXTS.document.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.document }
return FileAppearanceTypeEnum.custom
}
export const getSupportFileType = (fileName: string, fileMimetype: string, isCustom?: boolean) => {
if (isCustom) { return SupportUploadFileTypes.custom }
const extension = getFileExtension(fileName, fileMimetype)
for (const key in FILE_EXTS) {
if ((FILE_EXTS[key]).includes(extension.toUpperCase())) { return key }
}
return ''
}
export const getProcessedFiles = (files: FileEntity[]) => {
return files.filter(file => file.progress !== -1).map(fileItem => ({
type: fileItem.supportFileType,
transfer_method: fileItem.transferMethod,
url: fileItem.url || '',
upload_file_id: fileItem.uploadedId || '',
}))
}
export const getProcessedFilesFromResponse = (files: FileResponse[]) => {
return files.map((fileItem) => {
return {
id: fileItem.related_id,
name: fileItem.filename,
size: fileItem.size || 0,
type: fileItem.mime_type,
progress: 100,
transferMethod: fileItem.transfer_method,
supportFileType: fileItem.type,
uploadedId: fileItem.related_id,
url: fileItem.url,
}
})
}
export const getFileNameFromUrl = (url: string) => {
const urlParts = url.split('/')
return urlParts[urlParts.length - 1] || ''
}
export const getSupportFileExtensionList = (allowFileTypes: string[], allowFileExtensions: string[]) => {
if (allowFileTypes.includes(SupportUploadFileTypes.custom)) { return allowFileExtensions.map(item => item.slice(1).toUpperCase()) }
return allowFileTypes.map(type => FILE_EXTS[type]).flat()
}
export const isAllowedFileExtension = (fileName: string, fileMimetype: string, allowFileTypes: string[], allowFileExtensions: string[]) => {
return getSupportFileExtensionList(allowFileTypes, allowFileExtensions).includes(getFileExtension(fileName, fileMimetype).toUpperCase())
}
export const getFilesInLogs = (rawData: any) => {
const result = Object.keys(rawData || {}).map((key) => {
if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') {
return {
varName: key,
list: getProcessedFilesFromResponse([rawData[key]]),
}
}
if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) {
return {
varName: key,
list: getProcessedFilesFromResponse(rawData[key]),
}
}
return undefined
}).filter(Boolean)
return result
}
export const fileIsUploaded = (file: FileEntity) => {
if (file.uploadedId) { return true }
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100) { return true }
}
export const downloadFile = (url: string, filename: string) => {
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.style.display = 'none'
anchor.target = '_blank'
anchor.title = filename
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
}

View File

@ -2,12 +2,12 @@ import { forwardRef } from 'react'
import { generate } from './utils'
import type { AbstractNode } from './utils'
export type IconData = {
export interface IconData {
name: string
icon: AbstractNode
}
export type IconBaseProps = {
export interface IconBaseProps {
data: IconData
className?: string
onClick?: React.MouseEventHandler<SVGElement>

View File

@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "17",
"viewBox": "0 0 16 17",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Error"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M7.99992 5.83337V8.50004M7.99992 11.1667H8.00659M14.6666 8.50004C14.6666 12.1819 11.6818 15.1667 7.99992 15.1667C4.31802 15.1667 1.33325 12.1819 1.33325 8.50004C1.33325 4.81814 4.31802 1.83337 7.99992 1.83337C11.6818 1.83337 14.6666 4.81814 14.6666 8.50004Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "AlertCircle"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AlertCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'AlertCircle'
export default Icon

View File

@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "alert-triangle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M7.99977 5.33314V7.99981M7.99977 10.6665H8.00644M6.85977 1.90648L1.2131 11.3331C1.09668 11.5348 1.03508 11.7633 1.03443 11.9962C1.03378 12.229 1.0941 12.4579 1.20939 12.6602C1.32468 12.8624 1.49092 13.031 1.69157 13.149C1.89223 13.2671 2.1203 13.3306 2.3531 13.3331H13.6464C13.8792 13.3306 14.1073 13.2671 14.308 13.149C14.5086 13.031 14.6749 12.8624 14.7902 12.6602C14.9054 12.4579 14.9658 12.229 14.9651 11.9962C14.9645 11.7633 14.9029 11.5348 14.7864 11.3331L9.13977 1.90648C9.02092 1.71055 8.85358 1.54856 8.6539 1.43613C8.45422 1.32371 8.22893 1.26465 7.99977 1.26465C7.77061 1.26465 7.54532 1.32371 7.34564 1.43613C7.14596 1.54856 6.97862 1.71055 6.85977 1.90648Z",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "AlertTriangle"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AlertTriangle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'AlertTriangle'
export default Icon

View File

@ -0,0 +1,29 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4 14H10M10 14V20M10 14L3 21M20 10H14M14 10V4M14 10L21 3M20 14V16.8C20 17.9201 20 18.4802 19.782 18.908C19.5903 19.2843 19.2843 19.5903 18.908 19.782C18.4802 20 17.9201 20 16.8 20H14M10 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40973 4.40973 4.71569 4.21799 5.09202C4 5.51984 4 6.07989 4 7.2V10",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Collapse04"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Collapse04.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Collapse04'
export default Icon

View File

@ -0,0 +1,66 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "check-circle",
"clip-path": "url(#clip0_465_21765)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M4.37533 6.99984L6.12533 8.74984L9.62533 5.24984M12.8337 6.99984C12.8337 10.2215 10.222 12.8332 7.00033 12.8332C3.77866 12.8332 1.16699 10.2215 1.16699 6.99984C1.16699 3.77818 3.77866 1.1665 7.00033 1.1665C10.222 1.1665 12.8337 3.77818 12.8337 6.99984Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_465_21765"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "14",
"height": "14",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "CheckCircle"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './CheckCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'CheckCircle'
export default Icon

View File

@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "chevron-right"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M5.25 10.5L8.75 7L5.25 3.5",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "ChevronRight"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ChevronRight.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ChevronRight'
export default Icon

View File

@ -0,0 +1,29 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Clipboard"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Clipboard.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Clipboard'
export default Icon

View File

@ -0,0 +1,29 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9 15L11 17L15.5 12.5M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "ClipboardCheck"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ClipboardCheck.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ClipboardCheck'
export default Icon

View File

@ -0,0 +1,2 @@
export { default as ClipboardCheck } from './ClipboardCheck'
export { default as Clipboard } from './Clipboard'

View File

@ -0,0 +1,36 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "20",
"height": "20",
"viewBox": "0 0 20 20",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Retry"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M9.99996 1.66669C14.6023 1.66669 18.3333 5.39765 18.3333 10C18.3333 14.6024 14.6023 18.3334 9.99996 18.3334C5.39758 18.3334 1.66663 14.6024 1.66663 10H3.33329C3.33329 13.6819 6.31806 16.6667 9.99996 16.6667C13.6819 16.6667 16.6666 13.6819 16.6666 10C16.6666 6.31812 13.6819 3.33335 9.99996 3.33335C7.70848 3.33335 5.68702 4.48947 4.48705 6.25022L6.66663 6.25002V7.91669H1.66663V2.91669H3.33329L3.3332 4.99934C4.85358 2.97565 7.2739 1.66669 9.99996 1.66669Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "ReplayLine"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ReplayLine.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'ReplayLine'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "alert-circle"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Solid",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM8.66667 5.33329C8.66667 4.9651 8.36819 4.66663 8 4.66663C7.63181 4.66663 7.33334 4.9651 7.33334 5.33329V7.99996C7.33334 8.36815 7.63181 8.66663 8 8.66663C8.36819 8.66663 8.66667 8.36815 8.66667 7.99996V5.33329ZM8 9.99996C7.63181 9.99996 7.33334 10.2984 7.33334 10.6666C7.33334 11.0348 7.63181 11.3333 8 11.3333H8.00667C8.37486 11.3333 8.67334 11.0348 8.67334 10.6666C8.67334 10.2984 8.37486 9.99996 8.00667 9.99996H8Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "AlertCircle"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './AlertCircle.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'AlertCircle'
export default Icon

View File

@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "15",
"viewBox": "0 0 14 15",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon_2",
"d": "M11.6667 8.66667V10.3C11.6667 10.9534 11.6667 11.2801 11.5395 11.5297C11.4277 11.7492 11.2492 11.9277 11.0297 12.0395C10.7801 12.1667 10.4534 12.1667 9.8 12.1667H8.16667M5.83333 2.83333H4.2C3.54661 2.83333 3.21991 2.83333 2.97034 2.96049C2.75082 3.07234 2.57234 3.25082 2.46049 3.47034C2.33333 3.71991 2.33333 4.04661 2.33333 4.7V6.33333M8.75 5.75L12.25 2.25M12.25 2.25H8.75M12.25 2.25V5.75M5.25 9.25L1.75 12.75M1.75 12.75H5.25M1.75 12.75L1.75 9.25",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "Expand04"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Expand04.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Expand04'
export default Icon

View File

@ -1,6 +1,6 @@
import React from 'react'
export type AbstractNode = {
export interface AbstractNode {
name: string
attributes: {
[key: string]: string
@ -8,7 +8,7 @@ export type AbstractNode = {
children?: AbstractNode[]
}
export type Attrs = {
export interface Attrs {
[key: string]: string
}

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/answer"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M3.50114 1.67701L10.5011 1.677C11.5079 1.677 12.3241 2.49311 12.3241 3.49992V9.35414C12.3241 10.3609 11.5079 11.177 10.5012 11.1771H8.9954L7.41734 12.4845C7.17339 12.6866 6.81987 12.6856 6.57708 12.4821L5.02026 11.1771H3.50114C2.49436 11.1771 1.67822 10.3608 1.67822 9.35414V3.49993C1.67822 2.49316 2.49437 1.67701 3.50114 1.67701ZM10.5011 2.9895L3.50114 2.98951C3.21924 2.98951 2.99072 3.21803 2.99072 3.49993V9.35414C2.99072 9.63601 3.21926 9.86455 3.50114 9.86455H5.04675C5.33794 9.86455 5.61984 9.96705 5.84302 10.1541L7.00112 11.1249L8.17831 10.1496C8.40069 9.96537 8.68041 9.86455 8.96916 9.86455H10.5011C10.5011 9.86455 10.5011 9.86455 10.5011 9.86455C10.783 9.8645 11.0116 9.63592 11.0116 9.35414V3.49992C11.0116 3.21806 10.7831 2.9895 10.5011 2.9895ZM9.06809 4.93171C9.32437 5.18799 9.32437 5.60351 9.06809 5.85979L7.02642 7.90146C6.77014 8.15774 6.35464 8.15774 6.09835 7.90146L5.22333 7.02646C4.96704 6.77019 4.96704 6.35467 5.22332 6.09839C5.4796 5.8421 5.89511 5.8421 6.15139 6.09837L6.56238 6.50935L8.14001 4.93171C8.3963 4.67543 8.81181 4.67543 9.06809 4.93171Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Answer"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Answer.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Answer'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/code"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8.32593 1.69675C8.67754 1.78466 8.89132 2.14096 8.80342 2.49257L6.47009 11.8259C6.38218 12.1775 6.02588 12.3913 5.67427 12.3034C5.32265 12.2155 5.10887 11.8592 5.19678 11.5076L7.53011 2.17424C7.61801 1.82263 7.97431 1.60885 8.32593 1.69675ZM3.96414 4.20273C4.22042 4.45901 4.22042 4.87453 3.96413 5.13081L2.45578 6.63914C2.45577 6.63915 2.45578 6.63914 2.45578 6.63914C2.25645 6.83851 2.25643 7.16168 2.45575 7.36103C2.45574 7.36103 2.45576 7.36104 2.45575 7.36103L3.96413 8.86936C4.22041 9.12564 4.22042 9.54115 3.96414 9.79744C3.70787 10.0537 3.29235 10.0537 3.03607 9.79745L1.52769 8.28913C0.815811 7.57721 0.815803 6.42302 1.52766 5.7111L3.03606 4.20272C3.29234 3.94644 3.70786 3.94644 3.96414 4.20273ZM10.0361 4.20273C10.2923 3.94644 10.7078 3.94644 10.9641 4.20272L12.4725 5.71108C13.1843 6.423 13.1844 7.57717 12.4725 8.28909L10.9641 9.79745C10.7078 10.0537 10.2923 10.0537 10.036 9.79744C9.77977 9.54115 9.77978 9.12564 10.0361 8.86936L11.5444 7.36107C11.7437 7.16172 11.7438 6.83854 11.5444 6.63917C11.5444 6.63915 11.5445 6.63918 11.5444 6.63917L10.0361 5.13081C9.77978 4.87453 9.77978 4.45901 10.0361 4.20273Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Code"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Code.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Code'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/end"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M6.67315 1.18094C6.87691 1.0639 7.12769 1.06475 7.33067 1.18315L10.8307 3.22481C11.0323 3.34242 11.1562 3.55826 11.1562 3.79167C11.1562 4.02507 11.0323 4.24091 10.8307 4.35852L7.65625 6.21026V9.91667C7.65625 10.2791 7.36244 10.5729 7 10.5729C6.63756 10.5729 6.34375 10.2791 6.34375 9.91667V5.84577C6.34361 5.83788 6.34361 5.83 6.34375 5.82213V1.75C6.34375 1.51502 6.46939 1.29797 6.67315 1.18094ZM7.65625 4.69078L9.19758 3.79167L7.65625 2.89256V4.69078ZM5.31099 8.25466C5.37977 8.61051 5.14704 8.95473 4.79119 9.0235C3.97285 9.18165 3.32667 9.41764 2.90374 9.67762C2.45323 9.95454 2.40625 10.1564 2.40625 10.2086C2.40625 10.2448 2.42254 10.3508 2.60674 10.5202C2.79151 10.6901 3.09509 10.8732 3.52555 11.0406C4.38229 11.3738 5.61047 11.594 7 11.594C8.38954 11.594 9.61773 11.3738 10.4745 11.0406C10.9049 10.8732 11.2085 10.6901 11.3933 10.5202C11.5775 10.3508 11.5938 10.2448 11.5938 10.2086C11.5938 10.1564 11.5468 9.95454 11.0963 9.67762C10.6733 9.41764 10.0271 9.18165 9.20881 9.0235C8.85296 8.95473 8.62023 8.61051 8.68901 8.25465C8.75778 7.8988 9.102 7.66608 9.45786 7.73485C10.3682 7.91077 11.1803 8.18867 11.7836 8.55947C12.3592 8.91331 12.9062 9.45912 12.9062 10.2086C12.9062 10.7361 12.6287 11.1672 12.2816 11.4864C11.935 11.805 11.4698 12.0618 10.9502 12.2639C9.90679 12.6696 8.50997 12.9065 7 12.9065C5.49004 12.9065 4.09322 12.6696 3.04983 12.2639C2.53023 12.0618 2.06497 11.805 1.7184 11.4864C1.37128 11.1672 1.09375 10.7361 1.09375 10.2086C1.09375 9.45913 1.64077 8.91332 2.21642 8.55947C2.81966 8.18867 3.63181 7.91077 4.54215 7.73485C4.898 7.66608 5.24222 7.8988 5.31099 8.25466Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "End"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './End.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'End'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/home"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M6.99999 2.44562C6.97241 2.46663 6.94086 2.49116 6.90151 2.52177L3.43971 5.21428C3.17896 5.41708 3.15115 5.44593 3.13396 5.46918C3.10759 5.50483 3.08794 5.545 3.07599 5.58771C3.0682 5.61555 3.0625 5.65522 3.0625 5.98554V9.67837C3.0625 9.97506 3.06301 10.1581 3.07422 10.2954C3.08463 10.4228 3.10101 10.4541 3.10219 10.4563C3.13714 10.5249 3.19296 10.5808 3.26156 10.6157C3.2638 10.6169 3.29514 10.6333 3.42254 10.6437C3.55984 10.6549 3.74289 10.6555 4.03958 10.6555H4.8125V7.53462C4.8125 7.52831 4.81249 7.52199 4.81249 7.51565C4.81247 7.38933 4.81245 7.25834 4.82163 7.14594C4.8319 7.02025 4.85685 6.86124 4.93966 6.69872C5.05151 6.4792 5.22998 6.30072 5.44951 6.18886C5.61203 6.10605 5.77104 6.08111 5.89673 6.07084C6.00913 6.06166 6.14012 6.06168 6.26644 6.0617C6.27278 6.0617 6.2791 6.06171 6.28541 6.06171H7.71458C7.72089 6.06171 7.72721 6.0617 7.73355 6.0617C7.85987 6.06168 7.99086 6.06166 8.10326 6.07084C8.22896 6.08111 8.38796 6.10605 8.55049 6.18886C8.77001 6.30072 8.94849 6.4792 9.06034 6.69872C9.14315 6.86124 9.16809 7.02025 9.17836 7.14594C9.18755 7.25834 9.18752 7.38933 9.1875 7.51565C9.1875 7.52199 9.1875 7.52831 9.1875 7.53462V10.6555H9.96041C10.2571 10.6555 10.4402 10.6549 10.5775 10.6437C10.7049 10.6333 10.7361 10.6169 10.7383 10.6158C10.8069 10.5808 10.8628 10.525 10.8978 10.4564C10.8989 10.4541 10.9154 10.4228 10.9258 10.2954C10.937 10.1581 10.9375 9.97506 10.9375 9.67837V5.98554C10.9375 5.65522 10.9318 5.61555 10.924 5.58771C10.912 5.545 10.8924 5.50483 10.866 5.46918C10.8488 5.44593 10.821 5.41708 10.5603 5.21428L7.09848 2.52177C7.05913 2.49116 7.02757 2.46663 6.99999 2.44562ZM9.98433 11.968C10.2497 11.968 10.4871 11.968 10.6843 11.9519C10.8951 11.9346 11.1172 11.8958 11.3343 11.7852C11.6499 11.6244 11.9064 11.3678 12.0672 11.0523C12.1778 10.8351 12.2167 10.6131 12.2339 10.4023C12.25 10.205 12.25 9.96764 12.25 9.70225L12.25 5.98554C12.25 5.9671 12.25 5.94866 12.2501 5.93025C12.2504 5.69307 12.2508 5.45861 12.1879 5.23392C12.1329 5.03748 12.0426 4.85272 11.9213 4.68871C11.7825 4.50112 11.5972 4.35747 11.4098 4.21216C11.3952 4.20087 11.3806 4.18958 11.3661 4.17826L7.90428 1.48574C7.89214 1.4763 7.87933 1.46621 7.86587 1.4556C7.73357 1.35131 7.53852 1.19755 7.3049 1.1343C7.10523 1.08023 6.89477 1.08023 6.69509 1.1343C6.46148 1.19755 6.26642 1.35131 6.13412 1.4556C6.12066 1.46621 6.10785 1.4763 6.09571 1.48574L2.63391 4.17826C2.61935 4.18958 2.60478 4.20088 2.59022 4.21216C2.40278 4.35747 2.21747 4.50112 2.07873 4.68871C1.95742 4.85271 1.86706 5.03748 1.81207 5.23392C1.74918 5.4586 1.74956 5.69307 1.74994 5.93024C1.74997 5.94866 1.75 5.96709 1.75 5.98554L1.75 9.70227C1.74998 9.96765 1.74997 10.205 1.76608 10.4023C1.78331 10.6131 1.82216 10.8351 1.93279 11.0523C2.09357 11.3678 2.35014 11.6244 2.6657 11.7852C2.88282 11.8958 3.10485 11.9346 3.31566 11.9519C3.5129 11.968 3.75029 11.968 4.01566 11.968H9.98433ZM7.875 10.6555V7.53462C7.875 7.47093 7.87498 7.41945 7.87447 7.37473C7.82975 7.37422 7.77828 7.37421 7.71458 7.37421H6.28541C6.22172 7.37421 6.17024 7.37422 6.12553 7.37473C6.12501 7.41945 6.125 7.47093 6.125 7.53462V10.6555H7.875Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Home"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Home.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Home'
export default Icon

View File

@ -0,0 +1,71 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/http"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.0968 4.66675H10.8387V9.18288H11.7419V7.82804H13.0968C13.3362 7.82772 13.5658 7.73245 13.7351 7.56313C13.9044 7.39382 13.9997 7.16426 14 6.92481V5.56997C13.9997 5.33051 13.9045 5.10093 13.7351 4.9316C13.5658 4.76227 13.3362 4.66702 13.0968 4.66675ZM11.7419 6.92481V5.56997H13.0968L13.0972 6.92481H11.7419Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.06452 5.56997H4.96774V9.18288H5.87097V5.56997H6.77419V4.66675H4.06452V5.56997Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.93548 4.66675H7.22581V5.56997H8.12903V9.18288H9.03226V5.56997H9.93548V4.66675Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.25806 4.66675V6.4732H0.903226V4.66675H0V9.18288H0.903226V7.37643H2.25806V9.18288H3.16129V4.66675H2.25806Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Http"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Http.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Http'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/if-else"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8.16667 2.98975C7.80423 2.98975 7.51042 2.69593 7.51042 2.3335C7.51042 1.97106 7.80423 1.67725 8.16667 1.67725H11.0833C11.4458 1.67725 11.7396 1.97106 11.7396 2.3335V5.25016C11.7396 5.6126 11.4458 5.90641 11.0833 5.90641C10.7209 5.90641 10.4271 5.6126 10.4271 5.25016V3.91782L7.34474 7.00016L10.4271 10.0825V8.75016C10.4271 8.38773 10.7209 8.09391 11.0833 8.09391C11.4458 8.09391 11.7396 8.38773 11.7396 8.75016V11.6668C11.7396 12.0293 11.4458 12.3231 11.0833 12.3231H8.16667C7.80423 12.3231 7.51042 12.0293 7.51042 11.6668C7.51042 11.3044 7.80423 11.0106 8.16667 11.0106H9.49901L6.14484 7.65641H1.75C1.38756 7.65641 1.09375 7.3626 1.09375 7.00016C1.09375 6.63773 1.38756 6.34391 1.75 6.34391H6.14484L9.49901 2.98975H8.16667Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "IfElse"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './IfElse.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'IfElse'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/knowledge-retrieval"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M3.78528 2.62834C3.78527 2.62834 3.78528 2.62834 3.78528 2.62834L8 3.56494L12.2147 2.62834C13.5158 2.33921 14.75 3.32924 14.75 4.66206V11.2637C14.75 12.2401 14.0718 13.0855 13.1187 13.2974L8.1627 14.3987C8.05554 14.4225 7.94446 14.4225 7.8373 14.3987L2.88139 13.2974C1.92824 13.0855 1.25 12.2401 1.25 11.2637V4.66206C1.25 3.32925 2.4842 2.33921 3.78528 2.62834ZM7.25 4.93487L3.45988 4.09262C3.09558 4.01166 2.75 4.28887 2.75 4.66206V11.2637C2.75 11.537 2.93986 11.7738 3.20679 11.8331C3.20678 11.8331 3.20681 11.8331 3.20679 11.8331L7.25 12.7316V4.93487ZM8.75 12.7316L12.7932 11.8331C13.0601 11.7738 13.25 11.537 13.25 11.2637V4.66206C13.25 4.28887 12.9044 4.01165 12.5401 4.09262L8.75 4.93487V12.7316Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "KnowledgeRetrieval"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './KnowledgeRetrieval.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'KnowledgeRetrieval'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/llm"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M5.83333 2.40625C5.04971 2.40625 4.39011 2.94431 4.20689 3.67206C4.13982 3.93846 3.91391 4.1349 3.64078 4.16432C2.94692 4.23906 2.40625 4.82766 2.40625 5.54167C2.40625 5.92943 2.56471 6.27904 2.82212 6.53129C2.94807 6.65472 3.01905 6.82365 3.01905 7C3.01905 7.17635 2.94807 7.34528 2.82212 7.46871C2.56471 7.72096 2.40625 8.07057 2.40625 8.45833C2.40625 9.03652 2.76061 9.53347 3.26651 9.74092C3.45247 9.81717 3.59324 9.97444 3.64849 10.1677C3.8841 10.9917 4.64342 11.5938 5.54167 11.5938C5.82802 11.5938 6.09916 11.533 6.34375 11.4237V9.91667C6.34375 9.31258 5.85409 8.82292 5.25 8.82292C4.88756 8.82292 4.59375 8.5291 4.59375 8.16667C4.59375 7.80423 4.88756 7.51042 5.25 7.51042C5.64385 7.51042 6.0156 7.60503 6.34375 7.77278V2.48514C6.18319 2.43393 6.01183 2.40625 5.83333 2.40625ZM7.65625 2.48514V4.08333C7.65625 4.6874 8.14592 5.17708 8.75 5.17708C9.11244 5.17708 9.40625 5.4709 9.40625 5.83333C9.40625 6.19577 9.11244 6.48958 8.75 6.48958C8.35615 6.48958 7.9844 6.39496 7.65625 6.22722V11.4237C7.90087 11.533 8.17199 11.5938 8.45833 11.5938C9.35657 11.5938 10.1159 10.9917 10.3515 10.1677C10.4068 9.97444 10.5475 9.81717 10.7335 9.74092C11.2394 9.53347 11.5938 9.03652 11.5938 8.45833C11.5938 8.07056 11.4353 7.72096 11.1779 7.46871C11.0519 7.34528 10.981 7.17635 10.981 7C10.981 6.82365 11.0519 6.65472 11.1779 6.53129C11.4353 6.27904 11.5938 5.92944 11.5938 5.54167C11.5938 4.82766 11.0531 4.23906 10.3592 4.16432C10.0861 4.1349 9.86022 3.93847 9.79315 3.67208C9.6099 2.94432 8.95027 2.40625 8.16667 2.40625C7.98817 2.40625 7.81681 2.43393 7.65625 2.48514ZM7.00001 12.565C6.56031 12.7835 6.06472 12.9062 5.54167 12.9062C4.14996 12.9062 2.96198 12.0403 2.48457 10.8188C1.65595 10.3591 1.09375 9.47501 1.09375 8.45833C1.09375 7.9213 1.2511 7.42042 1.52161 7C1.2511 6.57958 1.09375 6.0787 1.09375 5.54167C1.09375 4.30153 1.93005 3.25742 3.06973 2.94157C3.51828 1.85715 4.586 1.09375 5.83333 1.09375C6.24643 1.09375 6.64104 1.17788 7 1.33013C7.35896 1.17788 7.75357 1.09375 8.16667 1.09375C9.41399 1.09375 10.4817 1.85716 10.9303 2.94157C12.0699 3.25742 12.9062 4.30153 12.9062 5.54167C12.9062 6.07869 12.7489 6.57958 12.4784 7C12.7489 7.42043 12.9062 7.92131 12.9062 8.45833C12.9062 9.47502 12.344 10.3591 11.5154 10.8188C11.038 12.0403 9.85003 12.9062 8.45833 12.9062C7.93526 12.9062 7.4397 12.7834 7.00001 12.565Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Llm"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Llm.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Llm'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/question-classifier"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M6.34379 3.53597C6.34379 2.35003 7.45832 1.47985 8.60885 1.76749L10.9422 2.35082C11.7537 2.55369 12.323 3.28283 12.323 4.1193V9.88081C12.323 10.7173 11.7537 11.4464 10.9422 11.6493L8.60886 12.2326C7.45832 12.5203 6.34379 11.6501 6.34379 10.4641V3.53597ZM8.29052 3.0408C7.96836 2.96026 7.65629 3.20392 7.65629 3.53597V10.4641C7.65629 10.7962 7.96836 11.0399 8.29051 10.9593L10.6238 10.376C10.6238 10.376 10.6238 10.376 10.6238 10.376C10.8511 10.3192 11.0105 10.115 11.0105 9.88081V4.1193C11.0105 3.88509 10.851 3.68093 10.6239 3.62413L8.29052 3.0408ZM4.66671 2.26048C5.02914 2.26048 5.32296 2.5543 5.32296 2.91673V11.0834C5.32296 11.4458 5.02914 11.7397 4.66671 11.7397C4.30427 11.7397 4.01046 11.4458 4.01046 11.0834V2.91673C4.01046 2.5543 4.30427 2.26048 4.66671 2.26048ZM2.33337 2.84382C2.69581 2.84382 2.98962 3.13763 2.98962 3.50007V10.5001C2.98962 10.8625 2.69581 11.1563 2.33337 11.1563C1.97094 11.1563 1.67712 10.8625 1.67712 10.5001V3.50007C1.67712 3.13763 1.97094 2.84382 2.33337 2.84382Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "QuestionClassifier"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './QuestionClassifier.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'QuestionClassifier'
export default Icon

View File

@ -0,0 +1,154 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/templating-transform"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M6.34375 1.75C6.34375 1.38756 6.63756 1.09375 7 1.09375C10.262 1.09375 12.9062 3.73807 12.9062 7C12.9062 10.262 10.262 12.9062 7 12.9062C6.63756 12.9062 6.34375 12.6124 6.34375 12.25V1.75Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.54167 3.64583C5.54167 3.968 5.2805 4.22917 4.95833 4.22917C4.63617 4.22917 4.375 3.968 4.375 3.64583C4.375 3.32367 4.63617 3.0625 4.95833 3.0625C5.2805 3.0625 5.54167 3.32367 5.54167 3.64583Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.5 3.64583C3.5 3.968 3.23883 4.22917 2.91667 4.22917C2.5945 4.22917 2.33333 3.968 2.33333 3.64583C2.33333 3.32367 2.5945 3.0625 2.91667 3.0625C3.23883 3.0625 3.5 3.32367 3.5 3.64583Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.54167 10.3542C5.54167 10.6763 5.2805 10.9375 4.95833 10.9375C4.63617 10.9375 4.375 10.6763 4.375 10.3542C4.375 10.032 4.63617 9.77083 4.95833 9.77083C5.2805 9.77083 5.54167 10.032 5.54167 10.3542Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.39583 1.89583C5.39583 2.13746 5.19996 2.33333 4.95833 2.33333C4.71671 2.33333 4.52083 2.13746 4.52083 1.89583C4.52083 1.65421 4.71671 1.45833 4.95833 1.45833C5.19996 1.45833 5.39583 1.65421 5.39583 1.89583Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1.75 5.83333C1.75 6.07495 1.55412 6.27083 1.3125 6.27083C1.07088 6.27083 0.875 6.07495 0.875 5.83333C0.875 5.59171 1.07088 5.39583 1.3125 5.39583C1.55412 5.39583 1.75 5.59171 1.75 5.83333Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1.75 8.16667C1.75 8.40828 1.55412 8.60417 1.3125 8.60417C1.07088 8.60417 0.875 8.40828 0.875 8.16667C0.875 7.92505 1.07088 7.72917 1.3125 7.72917C1.55412 7.72917 1.75 7.92505 1.75 8.16667Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.39583 12.1042C5.39583 12.3458 5.19996 12.5417 4.95833 12.5417C4.71671 12.5417 4.52083 12.3458 4.52083 12.1042C4.52083 11.8625 4.71671 11.6667 4.95833 11.6667C5.19996 11.6667 5.39583 11.8625 5.39583 12.1042Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.83333 5.83333C5.83333 6.31657 5.44158 6.70833 4.95833 6.70833C4.47508 6.70833 4.08333 6.31657 4.08333 5.83333C4.08333 5.35008 4.47508 4.95833 4.95833 4.95833C5.44158 4.95833 5.83333 5.35008 5.83333 5.83333Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.83333 8.16667C5.83333 8.6499 5.44158 9.04167 4.95833 9.04167C4.47508 9.04167 4.08333 8.6499 4.08333 8.16667C4.08333 7.68343 4.47508 7.29167 4.95833 7.29167C5.44158 7.29167 5.83333 7.68343 5.83333 8.16667Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.5 5.83333C3.5 6.15551 3.23883 6.41667 2.91667 6.41667C2.5945 6.41667 2.33333 6.15551 2.33333 5.83333C2.33333 5.51117 2.5945 5.25 2.91667 5.25C3.23883 5.25 3.5 5.51117 3.5 5.83333Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.5 8.16667C3.5 8.48884 3.23883 8.75 2.91667 8.75C2.5945 8.75 2.33333 8.48884 2.33333 8.16667C2.33333 7.84449 2.5945 7.58333 2.91667 7.58333C3.23883 7.58333 3.5 7.84449 3.5 8.16667Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.5 10.3542C3.5 10.6763 3.23883 10.9375 2.91667 10.9375C2.5945 10.9375 2.33333 10.6763 2.33333 10.3542C2.33333 10.032 2.5945 9.77083 2.91667 9.77083C3.23883 9.77083 3.5 10.032 3.5 10.3542Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "TemplatingTransform"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './TemplatingTransform.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'TemplatingTransform'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "icons/variable-x"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon (Stroke)",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M0.714375 3.42875C0.714375 2.22516 1.68954 1.25 2.89313 1.25C3.30734 1.25 3.64313 1.58579 3.64313 2C3.64313 2.41421 3.30734 2.75 2.89313 2.75C2.51796 2.75 2.21438 3.05359 2.21438 3.42875V6.28563C2.21438 6.48454 2.13536 6.6753 1.9947 6.81596L1.81066 7L1.9947 7.18404C2.13536 7.3247 2.21438 7.51546 2.21438 7.71437V10.5713C2.21438 10.9464 2.51796 11.25 2.89313 11.25C3.30734 11.25 3.64313 11.5858 3.64313 12C3.64313 12.4142 3.30734 12.75 2.89313 12.75C1.68954 12.75 0.714375 11.7748 0.714375 10.5713V8.02503L0.21967 7.53033C0.0790176 7.38968 0 7.19891 0 7C0 6.80109 0.0790176 6.61032 0.21967 6.46967L0.714375 5.97497V3.42875ZM10.3568 2C10.3568 1.58579 10.6925 1.25 11.1068 1.25C12.3103 1.25 13.2855 2.22516 13.2855 3.42875V5.97497L13.7802 6.46967C13.9209 6.61032 13.9999 6.80109 13.9999 7C13.9999 7.19891 13.9209 7.38968 13.7802 7.53033L13.2855 8.02503V10.5713C13.2855 11.7751 12.3095 12.75 11.1068 12.75C10.6925 12.75 10.3568 12.4142 10.3568 12C10.3568 11.5858 10.6925 11.25 11.1068 11.25C11.4815 11.25 11.7855 10.9462 11.7855 10.5713V7.71437C11.7855 7.51546 11.8645 7.3247 12.0052 7.18404L12.1892 7L12.0052 6.81596C11.8645 6.6753 11.7855 6.48454 11.7855 6.28563V3.42875C11.7855 3.05359 11.4819 2.75 11.1068 2.75C10.6925 2.75 10.3568 2.41421 10.3568 2ZM4.59467 4.59467C4.88756 4.30178 5.36244 4.30178 5.65533 4.59467L7 5.93934L8.34467 4.59467C8.63756 4.30178 9.11244 4.30178 9.40533 4.59467C9.69822 4.88756 9.69822 5.36244 9.40533 5.65533L8.06066 7L9.40533 8.34467C9.69822 8.63756 9.69822 9.11244 9.40533 9.40533C9.11244 9.69822 8.63756 9.69822 8.34467 9.40533L7 8.06066L5.65533 9.40533C5.36244 9.69822 4.88756 9.69822 4.59467 9.40533C4.30178 9.11244 4.30178 8.63756 4.59467 8.34467L5.93934 7L4.59467 5.65533C4.30178 5.36244 4.30178 4.88756 4.59467 4.59467Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "VariableX"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './VariableX.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'VariableX'
export default Icon

View File

@ -0,0 +1,11 @@
export { default as Answer } from './Answer'
export { default as Code } from './Code'
export { default as End } from './End'
export { default as Home } from './Home'
export { default as Http } from './Http'
export { default as IfElse } from './IfElse'
export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
export { default as Llm } from './Llm'
export { default as QuestionClassifier } from './QuestionClassifier'
export { default as TemplatingTransform } from './TemplatingTransform'
export { default as VariableX } from './VariableX'

View File

@ -5,7 +5,7 @@ import cn from 'classnames'
import s from './style.module.css'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type Props = {
interface Props {
srcs: string[]
}
@ -32,12 +32,15 @@ const ImageGallery: FC<Props> = ({
}) => {
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const imgNum = srcs.length
const validSrcs = srcs.filter(src => src && src.trim() !== '')
const imgNum = validSrcs.length
const imgStyle = getWidthStyle(imgNum)
if (imgNum === 0) { return null }
return (
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
{/* TODO: support preview */}
{srcs.map((src, index) => (
{validSrcs.map((src, index) => (
<img
key={index}
className={s.item}
@ -65,9 +68,9 @@ export const ImageGalleryTest = () => {
const imgGallerySrcs = (() => {
const srcs = []
for (let i = 0; i < 6; i++)
// srcs.push('https://placekitten.com/640/360')
// srcs.push('https://placekitten.com/360/640')
srcs.push('https://placekitten.com/360/360')
// srcs.push('https://placekitten.com/640/360')
// srcs.push('https://placekitten.com/360/640')
{ srcs.push('https://placekitten.com/360/360') }
return srcs
})()

View File

@ -13,7 +13,7 @@ import {
import Upload03 from '@/app/components/base/icons/line/upload-03'
import type { ImageFile, VisionSettings } from '@/types/app'
type UploadOnlyFromLocalProps = {
interface UploadOnlyFromLocalProps {
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
limit?: number
@ -39,7 +39,7 @@ const UploadOnlyFromLocal: FC<UploadOnlyFromLocalProps> = ({
)
}
type UploaderButtonProps = {
interface UploaderButtonProps {
methods: VisionSettings['transfer_methods']
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
@ -62,8 +62,7 @@ const UploaderButton: FC<UploaderButtonProps> = ({
}
const handleToggle = () => {
if (disabled)
return
if (disabled) { return }
setOpen(v => !v)
}
@ -115,7 +114,7 @@ const UploaderButton: FC<UploaderButtonProps> = ({
)
}
type ChatImageUploaderProps = {
interface ChatImageUploaderProps {
settings: VisionSettings
onUpload: (imageFile: ImageFile) => void
disabled?: boolean

View File

@ -5,7 +5,7 @@ import Button from '@/app/components/base/button'
import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
type ImageLinkInputProps = {
interface ImageLinkInputProps {
onUpload: (imageFile: ImageFile) => void
}
const regex = /^(https?|ftp):\/\//

View File

@ -10,7 +10,7 @@ import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type ImageListProps = {
interface ImageListProps {
list: ImageFile[]
readonly?: boolean
onRemove?: (imageFileId: string) => void
@ -31,12 +31,10 @@ const ImageList: FC<ImageListProps> = ({
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const handleImageLinkLoadSuccess = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1)
onImageLinkLoadSuccess(item._id)
if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1) { onImageLinkLoadSuccess(item._id) }
}
const handleImageLinkLoadError = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadError)
onImageLinkLoadError(item._id)
if (item.type === TransferMethod.remote_url && onImageLinkLoadError) { onImageLinkLoadError(item._id) }
}
return (

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import { createPortal } from 'react-dom'
import XClose from '@/app/components/base/icons/line/x-close'
type ImagePreviewProps = {
interface ImagePreviewProps {
url: string
onCancel: () => void
}

View File

@ -8,7 +8,7 @@ import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import Toast from '@/app/components/base/toast'
type UploaderProps = {
interface UploaderProps {
children: (hovering: boolean) => JSX.Element
onUpload: (imageFile: ImageFile) => void
limit?: number
@ -28,8 +28,7 @@ const Uploader: FC<UploaderProps> = ({
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
if (!file) { return }
if (limit && file.size > limit * 1024 * 1024) {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) })

View File

@ -2,7 +2,7 @@
import { upload } from '@/service/base'
type ImageUploadParams = {
interface ImageUploadParams {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void

View File

@ -2,7 +2,7 @@ import React from 'react'
import './style.css'
type ILoadingProps = {
interface ILoadingProps {
type?: 'area' | 'app'
}
const Loading = (

View File

@ -17,7 +17,7 @@ import {
import type { OffsetOptions, Placement } from '@floating-ui/react'
type PortalToFollowElemOptions = {
interface PortalToFollowElemOptions {
/*
* top, bottom, left, right
* start, end. Default is middle
@ -85,8 +85,7 @@ const PortalToFollowElemContext = React.createContext<ContextType>(null)
export function usePortalToFollowElemContext() {
const context = React.useContext(PortalToFollowElemContext)
if (context == null)
throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />')
if (context == null) { throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />') }
return context
}
@ -106,7 +105,7 @@ export function PortalToFollowElem({
}
export const PortalToFollowElemTrigger = React.forwardRef<
HTMLElement,
HTMLElement,
React.HTMLProps<HTMLElement> & { asChild?: boolean }
>(({ children, asChild = false, ...props }, propRef) => {
const context = usePortalToFollowElemContext()
@ -141,14 +140,13 @@ React.HTMLProps<HTMLElement> & { asChild?: boolean }
PortalToFollowElemTrigger.displayName = 'PortalToFollowElemTrigger'
export const PortalToFollowElemContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
>(({ style, ...props }, propRef) => {
const context = usePortalToFollowElemContext()
const ref = useMergeRefs([context.refs.setFloating, propRef])
if (!context.open)
return null
if (!context.open) { return null }
return (
<FloatingPortal>

View File

@ -0,0 +1,20 @@
interface ProgressBarProps {
percent: number
}
const ProgressBar = ({
percent = 0,
}: ProgressBarProps) => {
return (
<div className='flex items-center'>
<div className='mr-2 w-[100px] rounded-lg bg-gray-100'>
<div
className='h-1 rounded-lg bg-[#2970FF]'
style={{ width: `${percent}%` }}
/>
</div>
<div className='text-xs font-medium text-gray-500'>{percent}%</div>
</div>
)
}
export default ProgressBar

View File

@ -0,0 +1,64 @@
import { memo } from 'react'
import cn from '@/utils/classnames'
interface ProgressCircleProps {
className?: string
percentage?: number
size?: number
circleStrokeWidth?: number
circleStrokeColor?: string
circleFillColor?: string
sectorFillColor?: string
}
const ProgressCircle: React.FC<ProgressCircleProps> = ({
className,
percentage = 0,
size = 12,
circleStrokeWidth = 1,
circleStrokeColor = 'stroke-components-progress-brand-border',
circleFillColor = 'fill-components-progress-brand-bg',
sectorFillColor = 'fill-components-progress-brand-progress',
}) => {
const radius = size / 2
const center = size / 2
const angle = (percentage / 101) * 360
const radians = (angle * Math.PI) / 180
const x = center + radius * Math.cos(radians - Math.PI / 2)
const y = center + radius * Math.sin(radians - Math.PI / 2)
const largeArcFlag = percentage > 50 ? 1 : 0
const pathData = `
M ${center},${center}
L ${center},${center - radius}
A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y}
Z
`
return (
<svg
width={size + circleStrokeWidth}
height={size + circleStrokeWidth}
viewBox={`0 0 ${size + circleStrokeWidth} ${size + circleStrokeWidth}`}
className={className}
>
<circle
className={cn(
circleFillColor,
circleStrokeColor,
)}
cx={center + circleStrokeWidth / 2}
cy={center + circleStrokeWidth / 2}
r={radius}
strokeWidth={circleStrokeWidth}
/>
<path
className={cn(sectorFillColor)}
d={pathData}
transform={`translate(${circleStrokeWidth / 2}, ${circleStrokeWidth / 2})`}
/>
</svg>
)
}
export default memo(ProgressCircle)

View File

@ -15,12 +15,12 @@ const defaultItems = [
{ value: 7, name: 'option7' },
]
export type Item = {
export interface Item {
value: number | string
name: string
}
export type ISelectProps = {
export interface ISelectProps {
className?: string
items?: Item[]
defaultValue?: number | string
@ -45,8 +45,7 @@ const Select: FC<ISelectProps> = ({
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
if (existed) { defaultSelect = existed }
setSelectedItem(defaultSelect)
}, [defaultValue])
@ -77,23 +76,20 @@ const Select: FC<ISelectProps> = ({
? <Combobox.Input
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`}
onChange={(event) => {
if (!disabled)
setQuery(event.target.value)
if (!disabled) { setQuery(event.target.value) }
}}
displayValue={(item: Item) => item?.name}
/>
: <Combobox.Button onClick={
() => {
if (!disabled)
setOpen(!open)
if (!disabled) { setOpen(!open) }
}
} className={`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`}>
{selectedItem?.name}
</Combobox.Button>}
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={
() => {
if (!disabled)
setOpen(!open)
if (!disabled) { setOpen(!open) }
}
}>
{open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
@ -147,8 +143,7 @@ const SimpleSelect: FC<ISelectProps> = ({
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
if (existed) { defaultSelect = existed }
setSelectedItem(defaultSelect)
}, [defaultValue])

View File

@ -1,7 +1,7 @@
import type { FC } from 'react'
import React from 'react'
type Props = {
interface Props {
loading?: boolean
className?: string
children?: React.ReactNode | string

View File

@ -0,0 +1,18 @@
'use client'
import { Streamdown } from 'streamdown'
import 'katex/dist/katex.min.css'
interface StreamdownMarkdownProps {
content: string
className?: string
}
export function StreamdownMarkdown({ content, className = '' }: StreamdownMarkdownProps) {
return (
<div className={`streamdown-markdown ${className}`}>
<Streamdown>{content}</Streamdown>
</div>
)
}
export default StreamdownMarkdown

View File

@ -11,14 +11,14 @@ import {
} from '@heroicons/react/20/solid'
import { createContext, useContext } from 'use-context-selector'
export type IToastProps = {
export interface IToastProps {
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
message: string
children?: ReactNode
onClose?: () => void
}
type IToastContext = {
interface IToastContext {
notify: (props: IToastProps) => void
}
const defaultDuring = 3000
@ -33,8 +33,7 @@ const Toast = ({
children,
}: IToastProps) => {
// sometimes message is react node array. Not handle it.
if (typeof message !== 'string')
return null
if (typeof message !== 'string') { return null }
return <div className={classNames(
'fixed rounded-md p-4 my-4 mx-8 z-50',
@ -124,8 +123,7 @@ Toast.notify = ({
root.render(<Toast type={type} message={message} duration={duration} />)
document.body.appendChild(holder)
setTimeout(() => {
if (holder)
holder.remove()
if (holder) { holder.remove() }
}, duration || defaultDuring)
}
}

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useState } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
export type TooltipProps = {
export interface TooltipProps {
position?: 'top' | 'right' | 'bottom' | 'left'
triggerMethod?: 'hover' | 'click'
popupContent: React.ReactNode
@ -13,7 +13,7 @@ const arrow = (
<svg className="absolute text-white h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255"><polygon className="fill-current" points="0,0 127.5,127.5 255,0"></polygon></svg>
)
const Tooltip: FC< TooltipProps> = ({
const Tooltip: FC<TooltipProps> = ({
position = 'top',
triggerMethod = 'hover',
popupContent,

View File

@ -1,11 +1,10 @@
'use client'
import classNames from 'classnames'
import type { FC } from 'react'
import React from 'react'
import { Tooltip as ReactTooltip } from 'react-tooltip' // fixed version to 5.8.3 https://github.com/ReactTooltip/react-tooltip/issues/972
import 'react-tooltip/dist/react-tooltip.css'
import React, { useState } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
type TooltipProps = {
interface TooltipProps {
selector: string
content?: string
htmlContent?: React.ReactNode
@ -15,6 +14,10 @@ type TooltipProps = {
children: React.ReactNode
}
const arrow = (
<svg className="absolute text-white h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255"><polygon className="fill-current" points="0,0 127.5,127.5 255,0"></polygon></svg>
)
const Tooltip: FC<TooltipProps> = ({
selector,
content,
@ -24,22 +27,31 @@ const Tooltip: FC<TooltipProps> = ({
className,
clickable,
}) => {
const [open, setOpen] = useState(false)
const triggerMethod = clickable ? 'click' : 'hover'
return (
<div className='tooltip-container'>
{React.cloneElement(children as React.ReactElement, {
'data-tooltip-id': selector,
})
}
<ReactTooltip
id={selector}
content={content}
className={classNames('!bg-white !text-xs !font-normal !text-gray-700 !shadow-lg !opacity-100', className)}
place={position}
clickable={clickable}
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={position}
offset={10}
>
<PortalToFollowElemTrigger
data-selector={selector}
onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
onMouseEnter={() => triggerMethod === 'hover' && setOpen(true)}
onMouseLeave={() => triggerMethod === 'hover' && setOpen(false)}
>
{htmlContent && htmlContent}
</ReactTooltip>
</div>
{children}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[999]">
<div className={classNames('relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg', className)}>
{htmlContent ?? content}
{arrow}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

View File

@ -1,28 +1,32 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import LoadingAnim from '../loading-anim'
import type { FeedbackFunc, IChatItem } from '../type'
import s from '../style.module.css'
import ImageGallery from '../../base/image-gallery'
import Thought from '../thought'
import { randomString } from '@/utils/string'
import type { MessageRating, VisionFile } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip'
import { Markdown } from '@/app/components/base/markdown'
import type { FeedbackFunc } from '../type'
import type { ChatItem, MessageRating, VisionFile } from '@/types/app'
import type { Emoji } from '@/types/tools'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import StreamdownMarkdown from '@/app/components/base/streamdown-markdown'
import Tooltip from '@/app/components/base/tooltip'
import WorkflowProcess from '@/app/components/workflow/workflow-process'
import { randomString } from '@/utils/string'
import ImageGallery from '../../base/image-gallery'
import LoadingAnim from '../loading-anim'
import s from '../style.module.css'
import Thought from '../thought'
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
<div
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={onClick && onClick}
>
{innerContent}
</div>
)
function OperationBtn({ innerContent, onClick, className }: { innerContent: React.ReactNode, onClick?: () => void, className?: string }) {
return (
<div
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={onClick && onClick}
>
{innerContent}
</div>
)
}
const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -31,34 +35,41 @@ const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
)
const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
return isLike ? <HandThumbUpIcon className="w-4 h-4" /> : <HandThumbDownIcon className="w-4 h-4" />
}
const EditIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
</svg>
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
export const EditIconSolid: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fillRule="evenodd" clip-rule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fillRule="evenodd" clip-rule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
</svg>
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fillRule="evenodd" clip-rule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fillRule="evenodd" clip-rule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
</svg>
)
}
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
{children}
</div>
return (
<div className="rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100">
{children}
</div>
)
}
type IAnswerProps = {
item: IChatItem
interface IAnswerProps {
item: ChatItem
feedbackDisabled: boolean
onFeedback?: FeedbackFunc
isResponsing?: boolean
isResponding?: boolean
allToolIcons?: Record<string, string | Emoji>
suggestionClick?: (suggestion: string) => void
}
// The component needs to maintain its own state to control whether to display input component
@ -66,24 +77,24 @@ const Answer: FC<IAnswerProps> = ({
item,
feedbackDisabled = false,
onFeedback,
isResponsing,
isResponding,
allToolIcons,
suggestionClick = () => { },
}) => {
const { id, content, feedback, agent_thoughts } = item
const { id, content, feedback, agent_thoughts, workflowProcess, suggestedQuestions = [] } = item
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0
const { t } = useTranslation()
/**
* Render feedback results (distinguish between users and administrators)
* User reviews cannot be cancelled in Console
* @param rating feedback result
* @param isUserFeedback Whether it is user's feedback
* @returns comp
*/
* Render feedback results (distinguish between users and administrators)
* User reviews cannot be cancelled in Console
* @param rating feedback result
* @param isUserFeedback Whether it is user's feedback
* @returns comp
*/
const renderFeedbackRating = (rating: MessageRating | undefined) => {
if (!rating)
return null
if (!rating) { return null }
const isLike = rating === 'like'
const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200'
@ -94,7 +105,7 @@ const Answer: FC<IAnswerProps> = ({
content={isLike ? '取消赞同' : '取消反对'}
>
<div
className={'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800'}
className="relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800"
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={async () => {
await onFeedback?.(id, { rating: null })
@ -116,14 +127,16 @@ const Answer: FC<IAnswerProps> = ({
const userOperation = () => {
return feedback?.rating
? null
: <div className='flex gap-1'>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.like') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
</Tooltip>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.dislike') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
</Tooltip>
</div>
: (
<div className="flex gap-1">
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.like') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
</Tooltip>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.dislike') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
</Tooltip>
</div>
)
}
return (
@ -134,8 +147,7 @@ const Answer: FC<IAnswerProps> = ({
}
const getImgs = (list?: VisionFile[]) => {
if (!list)
return []
if (!list) { return [] }
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
}
@ -144,7 +156,7 @@ const Answer: FC<IAnswerProps> = ({
{agent_thoughts?.map((item, index) => (
<div key={index}>
{item.thought && (
<Markdown content={item.thought} />
<StreamdownMarkdown content={item.thought} />
)}
{/* {item.tool} */}
{/* perhaps not use tool */}
@ -152,7 +164,7 @@ const Answer: FC<IAnswerProps> = ({
<Thought
thought={item}
allToolIcons={allToolIcons || {}}
isFinished={!!item.observation || !isResponsing}
isFinished={!!item.observation || !isResponding}
/>
)}
@ -166,30 +178,45 @@ const Answer: FC<IAnswerProps> = ({
return (
<div key={id}>
<div className='flex items-start'>
<div className="flex items-start">
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponsing
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
{isResponding
&& (
<div className={s.typeingIcon}>
<LoadingAnim type="avatar" />
</div>
)}
</div>
<div className={`${s.answerWrap}`}>
<div className={`${s.answerWrap} max-w-[calc(100%-3rem)]`}>
<div className={`${s.answer} relative text-sm text-gray-900`}>
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
{(isResponsing && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
<div className={`ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl ${workflowProcess && 'min-w-[480px]'}`}>
{workflowProcess && (
<WorkflowProcess data={workflowProcess} hideInfo />
)}
{(isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
<div className="flex items-center justify-center w-6 h-5">
<LoadingAnim type="text" />
</div>
)
: (isAgentMode
? agentModeAnswer
: (
<Markdown content={content} />
<StreamdownMarkdown content={content} />
))}
{suggestedQuestions.length > 0 && (
<div className="mt-3">
<div className="flex gap-1 mt-1 flex-wrap">
{suggestedQuestions.map((suggestion, index) => (
<div key={index} className="flex items-center gap-1">
<Button className="text-sm" type="link" onClick={() => suggestionClick(suggestion)}>{suggestion}</Button>
</div>
))}
</div>
</div>
)}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
<div className="absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1">
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}
{/* User feedback must be displayed */}
{!feedbackDisabled && renderFeedbackRating(feedback?.rating)}

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