Browse Source

fix style
:
Signed-off-by: ryjiang <jiangruiyi@gmail.com>

ryjiang 1 month ago
parent
commit
1ac7943f62

+ 2 - 0
client/package.json

@@ -38,8 +38,10 @@
     "react-dom": "^18.2.0",
     "react-highlight-words": "^0.17.0",
     "react-i18next": "^13.5.0",
+    "react-markdown": "^10.1.0",
     "react-router-dom": "^6.23.1",
     "react-syntax-highlighter": "^15.6.1",
+    "remark-gfm": "^4.0.1",
     "remove": "^0.1.5",
     "socket.io-client": "^4.8.1",
     "web-vitals": "^1.0.1"

+ 17 - 1
client/src/components/icons/Icons.tsx

@@ -1029,7 +1029,23 @@ const icons: {
       ></path>
     </SvgIcon>
   ),
-  chat: (props = {}): ReactElement => <ChatIcon {...props} />,
+  chat: (props = {}) => (
+    <SvgIcon
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M12.5 3L2.5 3.00002C1.67157 3.00002 1 3.6716 1 4.50002V9.50003C1 10.3285 1.67157 11 2.5 11H7.50003C7.63264 11 7.75982 11.0527 7.85358 11.1465L10 13.2929V11.5C10 11.2239 10.2239 11 10.5 11H12.5C13.3284 11 14 10.3285 14 9.50003V4.5C14 3.67157 13.3284 3 12.5 3ZM2.49999 2.00002L12.5 2C13.8807 2 15 3.11929 15 4.5V9.50003C15 10.8807 13.8807 12 12.5 12H11V14.5C11 14.7022 10.8782 14.8845 10.6913 14.9619C10.5045 15.0393 10.2894 14.9965 10.1464 14.8536L7.29292 12H2.5C1.11929 12 0 10.8807 0 9.50003V4.50002C0 3.11931 1.11928 2.00003 2.49999 2.00002Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </SvgIcon>
+  ),
 };
 
 export default icons;

+ 209 - 119
client/src/pages/ai/AIChat.tsx

@@ -1,10 +1,24 @@
 import { FC, useState, useEffect, useRef } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Box, Typography, Chip, Stack } from '@mui/material';
+import {
+  Box,
+  Typography,
+  Chip,
+  Stack,
+  useTheme,
+  TextField,
+  Button,
+} from '@mui/material';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import { markdownStyles } from './styles/markdown';
 
 interface Message {
   id: string;
-  role: 'user' | 'assistant';
+  role: 'user' | 'assistant' | 'system';
   content: string;
   createdAt: string;
   parts?: Array<{
@@ -15,6 +29,7 @@ interface Message {
 
 const AIChat: FC = () => {
   const { t } = useTranslation('ai');
+  const theme = useTheme();
   const apiKey = localStorage.getItem('attu.ui.openai_api_key');
   const [messages, setMessages] = useState<Message[]>([]);
   const [input, setInput] = useState('');
@@ -23,6 +38,26 @@ const AIChat: FC = () => {
   const currentMessageRef = useRef<Message | null>(null);
   const accumulatedContentRef = useRef('');
 
+  const components = {
+    code({ node, inline, className, children, ...props }: any) {
+      const match = /language-(\w+)/.exec(className || '');
+      return !inline && match ? (
+        <SyntaxHighlighter
+          style={theme.palette.mode === 'dark' ? vscDarkPlus : oneLight}
+          language={match[1]}
+          PreTag="div"
+          {...props}
+        >
+          {String(children).replace(/\n$/, '')}
+        </SyntaxHighlighter>
+      ) : (
+        <code className={className} {...props}>
+          {children}
+        </code>
+      );
+    },
+  };
+
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     if (!input.trim() || isLoading) return;
@@ -53,7 +88,15 @@ const AIChat: FC = () => {
           'x-openai-api-key': apiKey || '',
         },
         body: JSON.stringify({
-          messages: [...messages, userMessage],
+          messages: [
+            {
+              role: 'system',
+              content:
+                'You are a helpful AI assistant specialized in Milvus and vector databases. You should only answer questions related to Milvus, vector databases, and vector search. For any other topics, please politely decline to answer and suggest asking about Milvus or vector databases instead.',
+            },
+            ...messages,
+            userMessage,
+          ],
         }),
       });
 
@@ -63,7 +106,17 @@ const AIChat: FC = () => {
 
       // Then create EventSource for streaming
       const eventSource = new EventSource(
-        `/api/v1/ai/chat?messages=${encodeURIComponent(JSON.stringify([...messages, userMessage]))}&x-openai-api-key=${encodeURIComponent(apiKey || '')}`
+        `/api/v1/ai/chat?messages=${encodeURIComponent(
+          JSON.stringify([
+            {
+              role: 'system',
+              content:
+                'You are a helpful AI assistant specialized in Milvus and vector databases. You should only answer questions related to Milvus, vector databases, and vector search. For any other topics, please politely decline to answer and suggest asking about Milvus or vector databases instead.',
+            },
+            ...messages,
+            userMessage,
+          ])
+        )}&x-openai-api-key=${encodeURIComponent(apiKey || '')}`
       );
       eventSourceRef.current = eventSource;
 
@@ -165,139 +218,176 @@ const AIChat: FC = () => {
 
   return (
     <Box
-      sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2 }}
+      sx={{
+        height: 'calc(100vh - 45px)',
+        display: 'flex',
+        flexDirection: 'column',
+      }}
     >
-      <Stack
-        direction="row"
-        justifyContent="space-between"
-        alignItems="center"
-        sx={{ mb: 2 }}
-      >
-        <Typography variant="h5">{t('title')}</Typography>
-        <Chip label={t('status.connected')} color="success" size="small" />
-      </Stack>
-
       <Box
         sx={{
           flex: 1,
           display: 'flex',
           flexDirection: 'column',
-          gap: 2,
-          mb: 2,
-          overflowY: 'auto',
+          overflow: 'hidden',
         }}
       >
-        {messages.map(message => (
-          <Box
-            key={message.id}
-            sx={{
-              display: 'flex',
-              flexDirection: 'column',
-              alignItems: message.role === 'user' ? 'flex-end' : 'flex-start',
-            }}
-          >
+        <Box
+          sx={{
+            flex: 1,
+            display: 'flex',
+            flexDirection: 'column',
+            gap: 2,
+            p: 2,
+            overflowY: 'auto',
+          }}
+        >
+          {messages.map(message => (
             <Box
+              key={message.id}
               sx={{
-                p: 2,
-                maxWidth: '80%',
-                borderRadius: 2,
-                backgroundColor:
-                  message.role === 'user' ? 'primary.main' : 'background.paper',
-                color:
-                  message.role === 'user'
-                    ? 'primary.contrastText'
-                    : 'text.primary',
-                boxShadow: 1,
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: message.role === 'user' ? 'flex-end' : 'flex-start',
+                mb: 2,
               }}
             >
-              {message.role === 'assistant' &&
-              message.id === currentMessageRef.current?.id ? (
-                <Typography variant="body1">{message.content || ''}</Typography>
-              ) : (
-                <Typography variant="body1">
-                  {message.parts?.map((part, index) => {
-                    if (part.type === 'text') {
-                      return <span key={index}>{part.text || ''}</span>;
-                    }
-                    return null;
-                  }) ||
-                    message.content ||
-                    ''}
-                </Typography>
-              )}
+              <Box
+                sx={{
+                  p: 2,
+                  maxWidth: '80%',
+                  borderRadius: 2,
+                  backgroundColor:
+                    message.role === 'user'
+                      ? theme.palette.mode === 'dark'
+                        ? 'primary.dark'
+                        : 'primary.main'
+                      : theme.palette.mode === 'dark'
+                        ? 'grey.800'
+                        : 'white',
+                  color:
+                    message.role === 'user'
+                      ? 'primary.contrastText'
+                      : theme.palette.mode === 'dark'
+                        ? 'grey.100'
+                        : 'text.primary',
+                  boxShadow: theme.palette.mode === 'dark' ? 2 : 1,
+                  overflow: 'hidden',
+                  ...markdownStyles(theme),
+                }}
+              >
+                {message.role === 'assistant' &&
+                message.id === currentMessageRef.current?.id ? (
+                  <Typography
+                    variant="body1"
+                    component="div"
+                    className="markdown-body"
+                  >
+                    <ReactMarkdown
+                      remarkPlugins={[remarkGfm]}
+                      components={components}
+                    >
+                      {message.content || ''}
+                    </ReactMarkdown>
+                  </Typography>
+                ) : (
+                  <Typography
+                    variant="body1"
+                    component="div"
+                    className="markdown-body"
+                  >
+                    {message.parts?.map((part, index) => {
+                      if (part.type === 'text') {
+                        return (
+                          <ReactMarkdown
+                            key={index}
+                            remarkPlugins={[remarkGfm]}
+                            components={components}
+                          >
+                            {part.text || ''}
+                          </ReactMarkdown>
+                        );
+                      }
+                      return null;
+                    }) || (
+                      <ReactMarkdown
+                        remarkPlugins={[remarkGfm]}
+                        components={components}
+                      >
+                        {message.content || ''}
+                      </ReactMarkdown>
+                    )}
+                  </Typography>
+                )}
+              </Box>
             </Box>
-          </Box>
-        ))}
-        {isLoading && (
-          <Box sx={{ display: 'flex', justifyContent: 'center' }}>
-            <Typography variant="body2" color="text.secondary">
-              {t('status.thinking')}
-            </Typography>
-          </Box>
-        )}
-      </Box>
-
-      {/* Suggested Commands */}
-      <Box sx={{ mb: 2 }}>
-        <Typography variant="subtitle2" sx={{ mb: 1 }}>
-          {t('suggestions.title')}
-        </Typography>
-        <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
-          {suggestedCommands.map((command, index) => (
-            <Chip
-              key={index}
-              label={command}
-              onClick={() => setInput(command)}
-              sx={{ mb: 1 }}
-            />
           ))}
-        </Stack>
-      </Box>
+          {isLoading && (
+            <Box sx={{ display: 'flex', justifyContent: 'center' }}>
+              <Typography variant="body2" color="text.secondary">
+                {t('status.thinking')}
+              </Typography>
+            </Box>
+          )}
+        </Box>
 
-      {/* Input Area */}
-      <Box
-        component="form"
-        onSubmit={handleSubmit}
-        sx={{
-          p: 2,
-          display: 'flex',
-          alignItems: 'center',
-          gap: 1,
-          backgroundColor: 'background.paper',
-          border: '1px solid',
-          borderColor: 'divider',
-          borderRadius: 1,
-        }}
-      >
-        <input
-          value={input}
-          onChange={handleInputChange}
-          placeholder={t('inputPlaceholder')}
-          disabled={isLoading}
-          style={{
-            flex: 1,
-            padding: '8px 12px',
-            border: '1px solid #ccc',
-            borderRadius: '4px',
-            backgroundColor: 'var(--mui-palette-background-default)',
-            color: 'var(--mui-palette-text-primary)',
+        {/* <Box
+          sx={{
+            p: 2,
+            borderTop: '1px solid',
+            borderColor: 'divider',
           }}
-        />
-        <button
-          type="submit"
-          disabled={isLoading || !input.trim()}
-          style={{
-            padding: '8px 16px',
-            backgroundColor: 'var(--mui-palette-primary-main)',
-            color: 'var(--mui-palette-primary-contrastText)',
-            border: 'none',
-            borderRadius: '4px',
-            cursor: 'pointer',
-            opacity: isLoading || !input.trim() ? 0.5 : 1,
+        >
+          <Typography variant="subtitle2" sx={{ mb: 1 }}>
+            {t('suggestions.title')}
+          </Typography>
+          <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
+            {suggestedCommands.map((command, index) => (
+              <Chip
+                key={index}
+                label={command}
+                onClick={() => setInput(command)}
+                sx={{ mb: 1 }}
+              />
+            ))}
+          </Stack>
+        </Box> */}
+
+        <Box
+          component="form"
+          onSubmit={handleSubmit}
+          sx={{
+            p: 2,
+            display: 'flex',
+            alignItems: 'center',
+            gap: 1,
+            backgroundColor: 'background.paper',
+            borderTop: '1px solid',
+            borderColor: 'divider',
           }}
         >
-          {t('send')}
-        </button>
+          <TextField
+            value={input}
+            onChange={handleInputChange}
+            placeholder={t('inputPlaceholder')}
+            disabled={isLoading}
+            fullWidth
+            size="small"
+            sx={{
+              '& .MuiOutlinedInput-root': {
+                backgroundColor: 'background.default',
+              },
+            }}
+          />
+          <Button
+            type="submit"
+            variant="contained"
+            disabled={isLoading || !input.trim()}
+            size="small"
+          >
+            {t('send')}
+          </Button>
+        </Box>
       </Box>
     </Box>
   );

+ 84 - 0
client/src/pages/ai/styles/markdown.ts

@@ -0,0 +1,84 @@
+import { Theme } from '@mui/material';
+
+export const markdownStyles = (theme: Theme) => ({
+  '& .markdown-body': {
+    backgroundColor: 'transparent',
+    fontSize: '14px',
+    '& pre': {
+      borderRadius: '6px',
+      padding: '16px',
+      overflow: 'auto',
+      backgroundColor: theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
+      color: theme.palette.mode === 'dark' ? 'grey.100' : 'text.primary',
+      margin: 0,
+    },
+    '& code': {
+      padding: '0.2em 0.4em',
+      borderRadius: '3px',
+      fontSize: '0.9em',
+      backgroundColor: theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
+      color: theme.palette.mode === 'dark' ? 'grey.100' : 'text.primary',
+    },
+    '& table': {
+      borderCollapse: 'collapse',
+      width: '100%',
+      margin: '16px 0',
+      color: 'inherit',
+      '& th, & td': {
+        border: `1px solid ${theme.palette.mode === 'dark' ? 'grey.700' : 'grey.300'}`,
+        padding: '8px',
+      },
+      '& th': {
+        backgroundColor:
+          theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100',
+      },
+      '& tr': {
+        '&:nth-of-type(even)': {
+          backgroundColor:
+            theme.palette.mode === 'dark'
+              ? 'rgba(255, 255, 255, 0.05)'
+              : 'rgba(0, 0, 0, 0.02)',
+        },
+        '&:hover': {
+          backgroundColor:
+            theme.palette.mode === 'dark'
+              ? 'rgba(255, 255, 255, 0.08)'
+              : 'rgba(0, 0, 0, 0.04)',
+        },
+      },
+    },
+    '& blockquote': {
+      margin: '16px 0',
+      padding: '0 16px',
+      borderLeft: `4px solid ${theme.palette.mode === 'dark' ? 'grey.700' : 'grey.300'}`,
+      color: theme.palette.mode === 'dark' ? 'grey.300' : 'text.secondary',
+    },
+    '& img': {
+      maxWidth: '100%',
+      height: 'auto',
+    },
+    '& a': {
+      color: theme.palette.mode === 'dark' ? 'primary.light' : 'primary.main',
+      textDecoration: 'none',
+      '&:hover': {
+        textDecoration: 'underline',
+      },
+    },
+    '& h1, & h2, & h3, & h4, & h5, & h6': {
+      color: 'inherit',
+      marginTop: '24px',
+      marginBottom: '16px',
+    },
+    '& p': {
+      color: 'inherit',
+      marginTop: '16px',
+      marginBottom: '16px',
+      '&:first-of-type': {
+        marginTop: 0,
+      },
+      '&:last-of-type': {
+        marginBottom: 0,
+      },
+    },
+  },
+});

File diff suppressed because it is too large
+ 763 - 1
client/yarn.lock


+ 4 - 6
server/src/ai/ai.service.ts

@@ -11,17 +11,15 @@ export class AIService {
         throw new Error('API key is required');
       }
 
-      // Initialize OpenAI client with DeepSeek configuration
+      // Initialize OpenAI client
       this.openai = new OpenAI({
-        baseURL: 'https://api.deepseek.com',
         apiKey: apiKey,
       });
 
       const stream = await this.openai.chat.completions.create({
-        model: 'deepseek-chat',
+        model: 'gpt-4.1-nano',
         messages: chatRequest.messages,
         stream: true,
-        max_tokens: 4096, // Default max output tokens for deepseek-chat
       });
 
       // Set headers for SSE
@@ -66,13 +64,13 @@ export class AIService {
       res.write('data: [DONE]\n\n');
       res.end();
     } catch (error) {
-      console.error('DeepSeek API error:', error);
+      console.error('API provider error:', error);
       if (!res.headersSent) {
         res.status(500).json({
           error:
             error instanceof Error
               ? error.message
-              : 'Failed to get response from DeepSeek',
+              : 'Failed to get response from API provider',
         });
       }
     }

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