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 (
+
+ );
+ },
+);
+Textarea.displayName = 'Textarea';
+
+export { Textarea };
diff --git a/src/components/ai/chat/utils.ts b/src/components/ai/chat/utils.ts
new file mode 100644
index 0000000..9ad0df4
--- /dev/null
+++ b/src/components/ai/chat/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}