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/.gitignore b/.gitignore index a112a9b..cc9c517 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,8 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -*storybook.log \ No newline at end of file +*storybook.log + +/.idea/ + +/.vscode/ 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..08b3a4c 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) @@ -43,6 +46,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -2082,6 +2088,9 @@ 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: { @@ -2643,6 +2652,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tooltip@1.1.2': + resolution: {integrity: sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: { @@ -2715,6 +2737,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.0': resolution: { @@ -12253,6 +12288,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 @@ -12667,6 +12704,26 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-tooltip@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 @@ -12707,6 +12764,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/rect@1.1.0': {} '@react-dev-inspector/babel-plugin@2.0.1': 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/app/edit/layout.tsx b/src/app/edit/layout.tsx index 9817fbf..a81e6cb 100644 --- a/src/app/edit/layout.tsx +++ b/src/app/edit/layout.tsx @@ -155,6 +155,7 @@ const Page: React.FC<{ children: React.ReactNode }> = ({ children }) => { 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 ( +