mirror of
https://github.com/langgenius/webapp-conversation.git
synced 2025-12-08 17:32:27 +08:00
Merge pull request #179 from Eric-Guo/main
Fix file upload (not image) in chat-flow.
This commit is contained in:
16
.cursor/rules/api-client.mdc
Normal file
16
.cursor/rules/api-client.mdc
Normal 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
10
.cursor/rules/i18n.mdc
Normal 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)`.
|
||||
21
.cursor/rules/project-structure.mdc
Normal file
21
.cursor/rules/project-structure.mdc
Normal 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)`
|
||||
20
.cursor/rules/typescript-react.mdc
Normal file
20
.cursor/rules/typescript-react.mdc
Normal 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.
|
||||
11
.cursor/rules/ui-components.mdc
Normal file
11
.cursor/rules/ui-components.mdc
Normal 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).
|
||||
@ -15,6 +15,9 @@ import Toast from '@/app/components/base/toast'
|
||||
import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader'
|
||||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import { useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
import FileUploaderInAttachmentWrapper from '@/app/components/base/file-uploader-in-attachment'
|
||||
import type { FileEntity, FileUpload } from '@/app/components/base/file-uploader-in-attachment/types'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader-in-attachment/utils'
|
||||
|
||||
export type IChatProps = {
|
||||
chatList: ChatItem[]
|
||||
@ -33,6 +36,7 @@ export type IChatProps = {
|
||||
isResponding?: boolean
|
||||
controlClearQuery?: number
|
||||
visionConfig?: VisionSettings
|
||||
fileConfig?: FileUpload
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
@ -46,6 +50,7 @@ const Chat: FC<IChatProps> = ({
|
||||
isResponding,
|
||||
controlClearQuery,
|
||||
visionConfig,
|
||||
fileConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = Toast
|
||||
@ -89,15 +94,20 @@ const Chat: FC<IChatProps> = ({
|
||||
onClear,
|
||||
} = useImageFiles()
|
||||
|
||||
const [attachmentFiles, setAttachmentFiles] = React.useState<FileEntity[]>([])
|
||||
|
||||
const handleSend = () => {
|
||||
if (!valid() || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(queryRef.current, files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
const imageFiles: VisionFile[] = files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))
|
||||
}))
|
||||
const docAndOtherFiles: VisionFile[] = getProcessedFiles(attachmentFiles)
|
||||
const combinedFiles: VisionFile[] = [...imageFiles, ...docAndOtherFiles]
|
||||
onSend(queryRef.current, combinedFiles)
|
||||
if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) {
|
||||
if (files.length)
|
||||
onClear()
|
||||
@ -106,6 +116,8 @@ const Chat: FC<IChatProps> = ({
|
||||
queryRef.current = ''
|
||||
}
|
||||
}
|
||||
if (!attachmentFiles.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId))
|
||||
setAttachmentFiles([])
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: any) => {
|
||||
@ -187,6 +199,17 @@ const Chat: FC<IChatProps> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
fileConfig?.enabled && (
|
||||
<div className={`${visionConfig?.enabled ? 'pl-[52px]' : ''} mb-1`}>
|
||||
<FileUploaderInAttachmentWrapper
|
||||
fileConfig={fileConfig}
|
||||
value={attachmentFiles}
|
||||
onChange={setAttachmentFiles}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Textarea
|
||||
className={`
|
||||
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
|
||||
|
||||
@ -12,6 +12,7 @@ import ConfigSence from '@/app/components/config-scence'
|
||||
import Header from '@/app/components/header'
|
||||
import { fetchAppParams, fetchChatList, fetchConversations, generationConversationName, sendChatMessage, updateFeedback } from '@/service'
|
||||
import type { ChatItem, ConversationItem, Feedbacktype, PromptConfig, VisionFile, VisionSettings } from '@/types/app'
|
||||
import type { FileUpload } from '@/app/components/base/file-uploader-in-attachment/types'
|
||||
import { Resolution, TransferMethod, WorkflowRunningStatus } from '@/types/app'
|
||||
import Chat from '@/app/components/chat'
|
||||
import { setLocaleOnClient } from '@/i18n/client'
|
||||
@ -48,6 +49,7 @@ const Main: FC<IMainProps> = () => {
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const [fileConfig, setFileConfig] = useState<FileUpload | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (APP_INFO?.title)
|
||||
@ -260,10 +262,20 @@ const Main: FC<IMainProps> = () => {
|
||||
prompt_template: promptTemplate,
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
const outerFileUploadEnabled = !!file_upload?.enabled
|
||||
setVisionConfig({
|
||||
...file_upload?.image,
|
||||
enabled: !!(outerFileUploadEnabled && file_upload?.image?.enabled),
|
||||
image_file_size_limit: system_parameters?.system_parameters || 0,
|
||||
})
|
||||
setFileConfig({
|
||||
enabled: outerFileUploadEnabled,
|
||||
allowed_file_types: file_upload?.allowed_file_types,
|
||||
allowed_file_extensions: file_upload?.allowed_file_extensions,
|
||||
allowed_file_upload_methods: file_upload?.allowed_file_upload_methods,
|
||||
number_limits: file_upload?.number_limits,
|
||||
fileUploadConfig: file_upload?.fileUploadConfig,
|
||||
})
|
||||
setConversationList(conversations as ConversationItem[])
|
||||
|
||||
if (isNotNewConversation)
|
||||
@ -373,7 +385,7 @@ const Main: FC<IMainProps> = () => {
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
}
|
||||
|
||||
if (visionConfig?.enabled && files && files?.length > 0) {
|
||||
if (files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
@ -391,7 +403,7 @@ const Main: FC<IMainProps> = () => {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
message_files: (files || []).filter((f: any) => f.type === 'image'),
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@ -700,6 +712,7 @@ const Main: FC<IMainProps> = () => {
|
||||
isResponding={isResponding}
|
||||
checkCanSend={checkCanSend}
|
||||
visionConfig={visionConfig}
|
||||
fileConfig={fileConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
|
||||
73
package.json
73
package.json
@ -12,73 +12,72 @@
|
||||
"prepare": "husky install ./.husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.2",
|
||||
"@formatjs/intl-localematcher": "^0.2.32",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@floating-ui/react": "^0.26.25",
|
||||
"@formatjs/intl-localematcher": "^0.5.6",
|
||||
"@headlessui/react": "2.2.1",
|
||||
"@heroicons/react": "^2.0.16",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tailwindcss/line-clamp": "^0.4.2",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/node": "~18.19.0",
|
||||
"@types/react": "~18.3.23",
|
||||
"@types/react-dom": "~18.3.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"ahooks": "^3.7.5",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.3.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.3.2",
|
||||
"classnames": "^2.5.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dify-client": "^2.3.1",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.4.0",
|
||||
"eslint": "~8.57.1",
|
||||
"eslint-config-next": "~14.2.32",
|
||||
"eventsource-parser": "^1.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"i18next": "^22.4.13",
|
||||
"i18next-resources-to-backend": "^1.1.3",
|
||||
"husky": "^9.1.7",
|
||||
"i18next": "^23.16.4",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"immer": "^9.0.19",
|
||||
"js-cookie": "^3.0.1",
|
||||
"katex": "^0.16.7",
|
||||
"katex": "^0.16.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime": "^4.0.7",
|
||||
"negotiator": "^0.6.3",
|
||||
"next": "^14.0.4",
|
||||
"next": "^14.2.32",
|
||||
"rc-textarea": "^1.5.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react": "~18.3.0",
|
||||
"react-dom": "~18.3.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-headless-pagination": "^1.1.4",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tooltip": "5.8.3",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"react-tooltip": "~5.8.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sass": "^1.61.0",
|
||||
"scheduler": "^0.23.0",
|
||||
"server-only": "^0.0.1",
|
||||
"swr": "^2.1.0",
|
||||
"swr": "^2.3.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"typescript": "4.9.5",
|
||||
"use-context-selector": "^1.4.1",
|
||||
"uuid": "^9.0.0",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "0.36.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@antfu/eslint-config": "~5.2.2",
|
||||
"@faker-js/faker": "^9.0.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/negotiator": "^0.6.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.7"
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.js?(x)": [
|
||||
|
||||
@ -61,6 +61,5 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/line-clamp'),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user