diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..a9139d8 --- /dev/null +++ b/.env.development @@ -0,0 +1,4 @@ +MODEL_API_BASE_URL=https://dashscope.aliyuncs.com/api/v1/apps/ + +QWEN_APP_ID= +QWEN_AUTH= diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..a9139d8 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +MODEL_API_BASE_URL=https://dashscope.aliyuncs.com/api/v1/apps/ + +QWEN_APP_ID= +QWEN_AUTH= diff --git a/.gitignore b/.gitignore index a112a9b..a5abcf0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -*storybook.log \ No newline at end of file +*storybook.log diff --git a/next.config.mjs b/next.config.mjs index 3cb2d9b..d4f818b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,6 +5,36 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, + compress: false, // 禁用gzip压缩 + async rewrites() { + return [ + { + source: '/ai/:path*', + destination: `${process.env.MODEL_API_BASE_URL}${process.env.QWEN_APP_ID}/:path*`, // 你想要代理的外部 API + }, + ]; + }, + async headers() { + return [ + { + source: '/ai/:path*', + headers: [ + { + key: 'Content-Type', + value: 'application/json', + }, + { + key: 'Authorization', + value: `Bearer ${process.env.QWEN_AUTH}`, + }, + { + key: 'Accept', + value: `text/event-stream`, + }, + ], + }, + ] + }, }; export default withNextDevtools(nextConfig); diff --git a/package.json b/package.json index 52b3ca2..ada5648 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@giscus/react": "^3.0.0", + "@microsoft/fetch-event-source": "^2.0.1", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-avatar": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e809412..64050b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: '@giscus/react': specifier: ^3.0.0 version: 3.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@microsoft/fetch-event-source': + specifier: ^2.0.1 + version: 2.0.1 '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.50.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1837,6 +1840,7 @@ packages: engines: { glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.2': resolution: @@ -1846,6 +1850,7 @@ packages: engines: { glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.2': resolution: @@ -1855,6 +1860,7 @@ packages: engines: { glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.2': resolution: @@ -1864,6 +1870,7 @@ packages: engines: { glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.2': resolution: @@ -1873,6 +1880,7 @@ packages: engines: { musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.2': resolution: @@ -1882,6 +1890,7 @@ packages: engines: { musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.4': resolution: @@ -1898,6 +1907,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.4': resolution: @@ -1914,6 +1924,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.4': resolution: @@ -1930,6 +1941,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.4': resolution: @@ -1946,6 +1958,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.4': resolution: @@ -1962,6 +1975,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.4': resolution: @@ -1978,6 +1992,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.4': resolution: @@ -2082,6 +2097,12 @@ packages: '@types/react': '>=16' react: '>=16' + '@microsoft/fetch-event-source@2.0.1': + resolution: + { + integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==, + } + '@monaco-editor/loader@1.4.0': resolution: { @@ -2160,6 +2181,7 @@ packages: engines: { node: '>= 10' } cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@14.2.4': resolution: @@ -2169,6 +2191,7 @@ packages: engines: { node: '>= 10' } cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@14.2.4': resolution: @@ -2178,6 +2201,7 @@ packages: engines: { node: '>= 10' } cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@14.2.4': resolution: @@ -2187,6 +2211,7 @@ packages: engines: { node: '>= 10' } cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@14.2.4': resolution: @@ -2795,6 +2820,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.18.1': resolution: @@ -2803,6 +2829,7 @@ packages: } cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.18.1': resolution: @@ -2811,6 +2838,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.18.1': resolution: @@ -2819,6 +2847,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.18.1': resolution: @@ -2827,6 +2856,7 @@ packages: } cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.18.1': resolution: @@ -2835,6 +2865,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.18.1': resolution: @@ -2843,6 +2874,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.18.1': resolution: @@ -2851,6 +2883,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.18.1': resolution: @@ -2859,6 +2892,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.18.1': resolution: @@ -12253,6 +12287,8 @@ snapshots: '@types/react': 18.3.3 react: 18.3.1 + '@microsoft/fetch-event-source@2.0.1': {} + '@monaco-editor/loader@1.4.0(monaco-editor@0.50.0)': dependencies: monaco-editor: 0.50.0 diff --git a/public/qwen.png b/public/qwen.png new file mode 100644 index 0000000..fb655a9 Binary files /dev/null and b/public/qwen.png differ diff --git a/src/app/edit/(main)/(router)/ai/page.tsx b/src/app/edit/(main)/(router)/ai/page.tsx index 8393456..992bcff 100644 --- a/src/app/edit/(main)/(router)/ai/page.tsx +++ b/src/app/edit/(main)/(router)/ai/page.tsx @@ -1,7 +1,13 @@ import React from 'react'; +import { ChatLayout } from '@/components/ai/chat/chat-layout'; + const AI = () => { - return
AI
; + return ( +
+ +
+ ); }; export default AI; diff --git a/src/components/ai/chat/chat-bottombar.tsx b/src/components/ai/chat/chat-bottombar.tsx new file mode 100644 index 0000000..1828015 --- /dev/null +++ b/src/components/ai/chat/chat-bottombar.tsx @@ -0,0 +1,85 @@ +import { FileImage, Paperclip, Send } from 'lucide-react'; +import React, { useRef, useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; + +import { Message, loggedInUserData } from './data'; +import { Textarea } from './textarea'; + +interface ChatBottombarProps { + sendMessage: (newMessage: Message) => void; +} + +export const BottombarIcons = [{ icon: FileImage }, { icon: Paperclip }]; + +export default function ChatBottombar({ sendMessage }: ChatBottombarProps) { + const [message, setMessage] = useState(''); + const inputRef = useRef(null); + + const handleInputChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + const handleSend = () => { + if (message.trim()) { + const newMessage: Message = { + id: Date.now(), + name: loggedInUserData.name, + avatar: loggedInUserData.avatar, + sessionId: loggedInUserData.sessionId, + message: message.trim(), + }; + sendMessage(newMessage); + setMessage(''); + + if (inputRef.current) { + inputRef.current.focus(); + } + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSend(); + } + + if (event.key === 'Enter' && event.shiftKey) { + event.preventDefault(); + setMessage((prev) => prev + '\n'); + } + }; + + return ( +
+ + + + + + +
+ ); +} diff --git a/src/components/ai/chat/chat-layout.tsx b/src/components/ai/chat/chat-layout.tsx new file mode 100644 index 0000000..f1dfe90 --- /dev/null +++ b/src/components/ai/chat/chat-layout.tsx @@ -0,0 +1,12 @@ +'use client'; + +import React from 'react'; + +import { userData } from './data'; +import { Chat } from './chat'; + +export function ChatLayout() { + const [selectedUser] = React.useState(userData[0]); + + return ; +} diff --git a/src/components/ai/chat/chat-list.tsx b/src/components/ai/chat/chat-list.tsx new file mode 100644 index 0000000..a9ee113 --- /dev/null +++ b/src/components/ai/chat/chat-list.tsx @@ -0,0 +1,76 @@ +import React, { useRef } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; + +import { Message, UserData } from './data'; +import { cn } from './utils'; +import ChatBottombar from './chat-bottombar'; + +import { Avatar, AvatarImage } from '@/components/ui/avatar/index'; + +interface ChatListProps { + messages?: Message[]; + selectedUser: UserData; + sendMessage: (newMessage: Message) => void; +} + +export function ChatList({ messages, selectedUser, sendMessage }: ChatListProps) { + const messagesContainerRef = useRef(null); + + React.useEffect(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }, [messages]); + + return ( +
+
+ + {messages?.map((message, index) => ( + +
+ {message.name !== selectedUser.name && ( + + + + )} + {message.message} + {message.name === selectedUser.name && ( + + + + )} +
+
+ ))} +
+
+ +
+ ); +} diff --git a/src/components/ai/chat/chat-topbar.tsx b/src/components/ai/chat/chat-topbar.tsx new file mode 100644 index 0000000..ba8bb5c --- /dev/null +++ b/src/components/ai/chat/chat-topbar.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { UserData } from './data'; + +import { Avatar, AvatarImage } from '@/components/ui/avatar/index'; + +interface ChatTopbarProps { + selectedUser: UserData; +} + +export default function ChatTopbar({ selectedUser }: ChatTopbarProps) { + return ( +
+
+ + + +
+ {selectedUser.name} + https://bailian.console.aliyun.com +
+
+
+ ); +} diff --git a/src/components/ai/chat/chat.tsx b/src/components/ai/chat/chat.tsx new file mode 100644 index 0000000..86c7eaa --- /dev/null +++ b/src/components/ai/chat/chat.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; + +import { Message, type UserData } from './data'; +import ChatTopbar from './chat-topbar'; +import { ChatList } from './chat-list'; + +interface ChatProps { + messages?: Message[]; + selectedUser: UserData; +} + +export function Chat({ messages, selectedUser }: ChatProps) { + const [messagesState, setMessages] = React.useState(messages ?? []); + + const sendMessage = (newMessage: Message) => { + setMessages((prevMessages) => [...prevMessages, newMessage]); + }; + + const updateMessage = (id: number, updatedFields: Partial) => { + setMessages((prevMessages) => + prevMessages.map((message) => + message.id === id ? { ...message, ...updatedFields } : message, + ), + ); + }; + + const fetchAnswer = (prompt: Message) => { + sendMessage(prompt); + + console.log('prompt'); + console.log(JSON.stringify(prompt)); + + const answerMessage: Message = { + id: Date.now() + 1, + name: 'GPT', + avatar: '/qwen.png', + sessionId: prompt.sessionId, + message: '', + }; + console.log('messageNew'); + console.log(JSON.stringify(answerMessage)); + sendMessage(answerMessage); + + const ctrl = new AbortController(); + fetchEventSource(`/ai/completion`, { + method: 'POST', + body: JSON.stringify({ + input: { + prompt: prompt.message, + session_id: prompt.sessionId, + }, + }), + signal: ctrl.signal, + onmessage(event) { + console.log(event); + + const data = JSON.parse(event.data); + const updatedMessage = data.output.text; + console.log(updatedMessage); + updateMessage(answerMessage.id, { message: updatedMessage }); + }, + }); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/components/ai/chat/data.tsx b/src/components/ai/chat/data.tsx new file mode 100644 index 0000000..9b59122 --- /dev/null +++ b/src/components/ai/chat/data.tsx @@ -0,0 +1,39 @@ +export const userData: UserData[] = [ + { + id: 1, + avatar: 'https://github.com/shadcn.png', + messages: [], + name: '通义千问', + }, +]; + +export type UserData = { + id: number; + avatar: string; + messages: Message[]; + name: string; +}; + +export const loggedInUserData = { + id: 5, + avatar: 'https://github.com/shadcn.png', + name: '通义千问', + sessionId: Date.now(), +}; + +export type LoggedInUserData = typeof loggedInUserData; + +export interface Message { + id: number; + avatar: string; + name: string; + message: string; + sessionId: number; +} + +export interface User { + id: number; + avatar: string; + messages: Message[]; + name: string; +} diff --git a/src/components/ai/chat/textarea.tsx b/src/components/ai/chat/textarea.tsx new file mode 100644 index 0000000..6caf454 --- /dev/null +++ b/src/components/ai/chat/textarea.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +import { cn } from './utils'; + +export interface TextareaProps extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +