Browse Source

stash

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
shanghaikid 1 month ago
parent
commit
741a019292

+ 4 - 1
client/package.json

@@ -6,6 +6,8 @@
   "bugs": "https://github.com/zilliztech/attu/issues",
   "bugs": "https://github.com/zilliztech/attu/issues",
   "private": false,
   "private": false,
   "dependencies": {
   "dependencies": {
+    "@ai-sdk/openai": "^1.3.22",
+    "@ai-sdk/react": "^1.2.12",
     "@codemirror/autocomplete": "^6.18.4",
     "@codemirror/autocomplete": "^6.18.4",
     "@codemirror/commands": "^6.6.0",
     "@codemirror/commands": "^6.6.0",
     "@codemirror/lang-javascript": "^6.2.2",
     "@codemirror/lang-javascript": "^6.2.2",
@@ -42,7 +44,8 @@
     "react-syntax-highlighter": "^15.6.1",
     "react-syntax-highlighter": "^15.6.1",
     "remove": "^0.1.5",
     "remove": "^0.1.5",
     "socket.io-client": "^4.8.1",
     "socket.io-client": "^4.8.1",
-    "web-vitals": "^1.0.1"
+    "web-vitals": "^1.0.1",
+    "zod": "^3.25.45"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@lezer/generator": "^1.7.2",
     "@lezer/generator": "^1.7.2",

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

@@ -6,6 +6,8 @@ import KeyIcon from '@/assets/icons/key.svg?react';
 import SearchEmptyIcon from '@/assets/icons/search.svg?react';
 import SearchEmptyIcon from '@/assets/icons/search.svg?react';
 import Compact from '@/assets/icons/compact.svg?react';
 import Compact from '@/assets/icons/compact.svg?react';
 import type { IconsType } from './Types';
 import type { IconsType } from './Types';
+import { ReactElement } from 'react';
+import { Chat as ChatIcon } from '@mui/icons-material';
 
 
 const icons: {
 const icons: {
   [x in IconsType]: (props?: SvgIconProps) => React.ReactElement;
   [x in IconsType]: (props?: SvgIconProps) => React.ReactElement;
@@ -969,7 +971,7 @@ const icons: {
       xmlns="http://www.w3.org/2000/svg"
       xmlns="http://www.w3.org/2000/svg"
     >
     >
       <path
       <path
-        d="M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V6H8.5C8.22386 6 8 5.77614 8 5.5V2H3.5ZM9 2.70711L11.2929 5H9V2.70711ZM2 2.5C2 1.67157 2.67157 1 3.5 1H8.5C8.63261 1 8.75979 1.05268 8.85355 1.14645L12.8536 5.14645C12.9473 5.24021 13 5.36739 13 5.5V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5Z"
+        d="M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V4.70711L9.29289 2H3.5ZM2 2.5C2 1.67157 2.67157 1 3.5 1H9.5C9.63261 1 9.75979 1.05268 9.85355 1.14645L12.7803 4.07322C12.921 4.21388 13 4.40464 13 4.60355V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5Z"
         fill="currentColor"
         fill="currentColor"
         fillRule="evenodd"
         fillRule="evenodd"
         clipRule="evenodd"
         clipRule="evenodd"
@@ -1027,6 +1029,7 @@ const icons: {
       ></path>
       ></path>
     </SvgIcon>
     </SvgIcon>
   ),
   ),
+  chat: (props = {}): ReactElement => <ChatIcon {...props} />,
 };
 };
 
 
 export default icons;
 export default icons;

+ 2 - 1
client/src/components/icons/Types.ts

@@ -62,4 +62,5 @@ export type IconsType =
   | 'file'
   | 'file'
   | 'eye'
   | 'eye'
   | 'newWindow'
   | 'newWindow'
-  | 'caretSort';
+  | 'caretSort'
+  | 'chat';

+ 24 - 0
client/src/components/layout/Header.tsx

@@ -21,6 +21,7 @@ import {
 } from '@/context';
 } from '@/context';
 import { MilvusService } from '@/http';
 import { MilvusService } from '@/http';
 import UpdateUser from '@/pages/user/dialogs/UpdateUserPassDialog';
 import UpdateUser from '@/pages/user/dialogs/UpdateUserPassDialog';
+import UpdateApiKeyDialog from '@/pages/user/dialogs/UpdateApiKeyDialog';
 import icons from '../icons/Icons';
 import icons from '../icons/Icons';
 import Breadcrumbs from '@mui/material/Breadcrumbs';
 import Breadcrumbs from '@mui/material/Breadcrumbs';
 
 
@@ -93,6 +94,26 @@ const Header: FC = () => {
     });
     });
   };
   };
 
 
+  const handleSetApiKey = () => {
+    setUserAnchorEl(null);
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component: (
+          <UpdateApiKeyDialog
+            open={true}
+            onClose={handleCloseDialog}
+            onSave={(apiKey) => {
+              openSnackBar(successTrans('apiKeySaved'));
+              handleCloseDialog();
+            }}
+          />
+        ),
+      },
+    });
+  };
+
   const handleDbClick = (event: MouseEvent<HTMLElement>) => {
   const handleDbClick = (event: MouseEvent<HTMLElement>) => {
     setDbAnchorEl(event.currentTarget);
     setDbAnchorEl(event.currentTarget);
   };
   };
@@ -246,6 +267,9 @@ const Header: FC = () => {
                   <MenuItem onClick={handleChangePassword}>
                   <MenuItem onClick={handleChangePassword}>
                     {userTrans('changePassword')}
                     {userTrans('changePassword')}
                   </MenuItem>
                   </MenuItem>
+                  <MenuItem onClick={handleSetApiKey}>
+                    {userTrans('setApiKey')}
+                  </MenuItem>
                 </Menu>
                 </Menu>
               </>
               </>
             )}
             )}

+ 16 - 0
client/src/config/routes.ts

@@ -6,6 +6,7 @@ import Users from '@/pages/user/UsersAndRoles';
 import System from '@/pages/system/SystemView';
 import System from '@/pages/system/SystemView';
 import Play from '@/pages/play/Play';
 import Play from '@/pages/play/Play';
 import Overview from '@/pages/home/Home';
 import Overview from '@/pages/home/Home';
+import AIChat from '@/pages/ai/AIChat';
 
 
 // Route path constants
 // Route path constants
 export const ROUTE_PATHS = {
 export const ROUTE_PATHS = {
@@ -19,6 +20,7 @@ export const ROUTE_PATHS = {
   PLAY: 'play',
   PLAY: 'play',
   SYSTEM: 'system',
   SYSTEM: 'system',
   CONNECT: 'connect',
   CONNECT: 'connect',
+  AI_CHAT: 'ai-chat',
 } as const;
 } as const;
 
 
 export type RoutePath = (typeof ROUTE_PATHS)[keyof typeof ROUTE_PATHS];
 export type RoutePath = (typeof ROUTE_PATHS)[keyof typeof ROUTE_PATHS];
@@ -202,6 +204,20 @@ const otherRoutes: RouteItem[] = [
     requiresAuth: false,
     requiresAuth: false,
     routerType: ROUTE_PATHS.HOME,
     routerType: ROUTE_PATHS.HOME,
   },
   },
+  {
+    path: ROUTE_PATHS.AI_CHAT,
+    element: AIChat,
+    showInMenu: true,
+    menuConfig: {
+      icon: icons.chat,
+      label: 'aiChat',
+      key: 'ai-chat',
+    },
+    routerType: ROUTE_PATHS.AI_CHAT,
+    navConfig: {
+      navTitleKey: 'aiChat',
+    },
+  },
 ];
 ];
 
 
 // Combine all routes
 // Combine all routes

+ 5 - 0
client/src/i18n/cn/user.ts

@@ -68,6 +68,11 @@ const userTrans = {
   DatabasePrivilegeGroups: '内置权限组',
   DatabasePrivilegeGroups: '内置权限组',
   ClusterPrivilegeGroups: '内置权限组',
   ClusterPrivilegeGroups: '内置权限组',
   CustomPrivilegeGroups: '用户定义权限组',
   CustomPrivilegeGroups: '用户定义权限组',
+
+  // API Key
+  setApiKey: '设置 OpenAI API Key',
+  apiKey: 'OpenAI API Key',
+  apiKeyHelper: '输入您的 OpenAI API key 以启用 AI 聊天功能',
 };
 };
 
 
 export default userTrans;
 export default userTrans;

+ 5 - 0
client/src/i18n/en/user.ts

@@ -72,6 +72,11 @@ const userTrans = {
   DatabasePrivilegeGroups: 'Built-in Groups',
   DatabasePrivilegeGroups: 'Built-in Groups',
   ClusterPrivilegeGroups: 'Built-in Groups',
   ClusterPrivilegeGroups: 'Built-in Groups',
   CustomPrivilegeGroups: 'User-defined Groups',
   CustomPrivilegeGroups: 'User-defined Groups',
+
+  // API Key
+  setApiKey: 'Set OpenAI API Key',
+  apiKey: 'OpenAI API Key',
+  apiKeyHelper: 'Enter your OpenAI API key to enable AI chat features',
 };
 };
 
 
 export default userTrans;
 export default userTrans;

+ 162 - 0
client/src/pages/ai/AIChat.tsx

@@ -0,0 +1,162 @@
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Box, Typography, Chip, Stack } from '@mui/material';
+import { useChat } from '@ai-sdk/react';
+
+const AIChat: FC = () => {
+  const { t } = useTranslation('ai');
+  const apiKey = localStorage.getItem('attu.ui.openai_api_key');
+
+  const { messages, input, handleInputChange, handleSubmit, isLoading } =
+    useChat({
+      api: '/api/v1/ai/chat',
+      headers: {
+        'x-openai-api-key': apiKey || '',
+      },
+      onError: (error: Error) => {
+        console.error('Chat error:', error);
+      },
+    });
+
+  const suggestedCommands = [
+    t('suggestions.search'),
+    t('suggestions.create'),
+    t('suggestions.delete'),
+    t('suggestions.insert'),
+  ];
+
+  return (
+    <Box
+      sx={{ height: '100%', display: 'flex', flexDirection: 'column', p: 2 }}
+    >
+      <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,
+        }}
+      >
+        {messages.map(message => (
+          <Box
+            key={message.id}
+            sx={{
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: message.role === 'user' ? 'flex-end' : 'flex-start',
+            }}
+          >
+            <Box
+              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,
+              }}
+            >
+              <Typography variant="body1">
+                {message.parts.map((part, index) => {
+                  if (part.type === 'text') {
+                    return <span key={index}>{part.text}</span>;
+                  }
+                  return null;
+                })}
+              </Typography>
+            </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={() =>
+                handleInputChange({ target: { value: command } } as any)
+              }
+              sx={{ mb: 1 }}
+            />
+          ))}
+        </Stack>
+      </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)',
+          }}
+        />
+        <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,
+          }}
+        >
+          {t('send')}
+        </button>
+      </Box>
+    </Box>
+  );
+};
+
+export default AIChat;

+ 41 - 0
client/src/pages/user/dialogs/UpdateApiKeyDialog.tsx

@@ -0,0 +1,41 @@
+import { FC, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { TextField, Box } from '@mui/material';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+
+interface Props {
+  open: boolean;
+  onClose: () => void;
+  onSave: (apiKey: string) => void;
+}
+
+const UpdateApiKeyDialog: FC<Props> = ({ onClose, onSave }) => {
+  const { t } = useTranslation('user');
+  const [apiKey, setApiKey] = useState(localStorage.getItem('attu.ui.openai_api_key') || '');
+
+  const handleSave = async () => {
+    localStorage.setItem('attu.ui.openai_api_key', apiKey);
+    onSave(apiKey);
+  };
+
+  return (
+    <DialogTemplate
+      title={t('setApiKey')}
+      handleClose={onClose}
+      handleConfirm={handleSave}
+    >
+      <Box sx={{ mt: 2 }}>
+        <TextField
+          fullWidth
+          label={t('apiKey')}
+          value={apiKey}
+          onChange={(e) => setApiKey(e.target.value)}
+          type="password"
+          helperText={t('apiKeyHelper')}
+        />
+      </Box>
+    </DialogTemplate>
+  );
+};
+
+export default UpdateApiKeyDialog; 

+ 86 - 0
client/yarn.lock

@@ -2,6 +2,49 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
+"@ai-sdk/openai@^1.3.22":
+  version "1.3.22"
+  resolved "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.22.tgz#ed52af8f8fb3909d108e945d12789397cb188b9b"
+  integrity sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==
+  dependencies:
+    "@ai-sdk/provider" "1.1.3"
+    "@ai-sdk/provider-utils" "2.2.8"
+
+"@ai-sdk/provider-utils@2.2.8":
+  version "2.2.8"
+  resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1"
+  integrity sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==
+  dependencies:
+    "@ai-sdk/provider" "1.1.3"
+    nanoid "^3.3.8"
+    secure-json-parse "^2.7.0"
+
+"@ai-sdk/provider@1.1.3":
+  version "1.1.3"
+  resolved "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz#ebdda8077b8d2b3f290dcba32c45ad19b2704681"
+  integrity sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==
+  dependencies:
+    json-schema "^0.4.0"
+
+"@ai-sdk/react@^1.2.12":
+  version "1.2.12"
+  resolved "https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.12.tgz#f4250b6df566b170af98a71d5708b52108dd0ce1"
+  integrity sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==
+  dependencies:
+    "@ai-sdk/provider-utils" "2.2.8"
+    "@ai-sdk/ui-utils" "1.2.11"
+    swr "^2.2.5"
+    throttleit "2.1.0"
+
+"@ai-sdk/ui-utils@1.2.11":
+  version "1.2.11"
+  resolved "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz#4f815589d08d8fef7292ade54ee5db5d09652603"
+  integrity sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==
+  dependencies:
+    "@ai-sdk/provider" "1.1.3"
+    "@ai-sdk/provider-utils" "2.2.8"
+    zod-to-json-schema "^3.24.1"
+
 "@ampproject/remapping@^2.2.0":
 "@ampproject/remapping@^2.2.0":
   version "2.3.0"
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
   resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
@@ -2171,6 +2214,11 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
   integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
 
 
+dequal@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
 dom-helpers@^5.0.1:
 dom-helpers@^5.0.1:
   version "5.2.1"
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@@ -2934,6 +2982,11 @@ json-parse-even-better-errors@^2.3.0:
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
 
+json-schema@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
+  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
+
 json5@^2.2.3:
 json5@^2.2.3:
   version "2.2.3"
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
@@ -3461,6 +3514,11 @@ scheduler@^0.23.2:
   dependencies:
   dependencies:
     loose-envify "^1.1.0"
     loose-envify "^1.1.0"
 
 
+secure-json-parse@^2.7.0:
+  version "2.7.0"
+  resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
+  integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
+
 semver@^6.3.1:
 semver@^6.3.1:
   version "6.3.1"
   version "6.3.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -3645,6 +3703,19 @@ svg-parser@^2.0.4:
   resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
   resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
   integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
   integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
 
 
+swr@^2.2.5:
+  version "2.3.3"
+  resolved "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz#9d6a703355f15f9099f45114db3ef75764444788"
+  integrity sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==
+  dependencies:
+    dequal "^2.0.3"
+    use-sync-external-store "^1.4.0"
+
+throttleit@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4"
+  integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==
+
 tiny-warning@^1.0.2:
 tiny-warning@^1.0.2:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
@@ -3769,6 +3840,11 @@ update-browserslist-db@^1.1.1:
     escalade "^3.2.0"
     escalade "^3.2.0"
     picocolors "^1.1.1"
     picocolors "^1.1.1"
 
 
+use-sync-external-store@^1.4.0:
+  version "1.5.0"
+  resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
+  integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
+
 vite-plugin-svgr@^4.2.0:
 vite-plugin-svgr@^4.2.0:
   version "4.2.0"
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz#9f3bf5206b0ec510287e56d16f1915e729bb4e6b"
   resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz#9f3bf5206b0ec510287e56d16f1915e729bb4e6b"
@@ -3881,3 +3957,13 @@ yaml@^1.10.0:
   version "1.10.2"
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
   integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+zod-to-json-schema@^3.24.1:
+  version "3.24.5"
+  resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
+  integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
+
+zod@^3.25.45:
+  version "3.25.45"
+  resolved "https://registry.npmjs.org/zod/-/zod-3.25.45.tgz#f4915644fc614baaad027d3c2b127375d9d62b23"
+  integrity sha512-kv1swJBZqv98NQibL0oVvkQE8rXT+6qGNM1FpZkFcJG2jnz4vbtu48bgaitp85CaBPLSKXibrEsU7MzJoVoZAA==

+ 1 - 0
server/package.json

@@ -29,6 +29,7 @@
     "lru-cache": "^10.2.0",
     "lru-cache": "^10.2.0",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "node-cron": "^3.0.3",
     "node-cron": "^3.0.3",
+    "openai": "^5.0.1",
     "socket.io": "^4.8.1"
     "socket.io": "^4.8.1"
   },
   },
   "jest": {
   "jest": {

+ 37 - 0
server/src/ai/ai.controller.ts

@@ -0,0 +1,37 @@
+import { NextFunction, Request, Response, Router } from 'express';
+import { AIService } from './ai.service';
+import { ChatRequestDto } from './dto/chat-request.dto';
+
+export class AIController {
+  private aiService: AIService;
+  private router: Router;
+
+  constructor() {
+    this.aiService = new AIService();
+    this.router = Router();
+  }
+
+  generateRoutes() {
+    this.router.post('/chat', this.handleChat.bind(this));
+    return this.router;
+  }
+
+  async handleChat(
+    req: Request<{}, {}, ChatRequestDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    try {
+      const apiKey = req.headers['x-openai-api-key'] as string;
+      if (!apiKey) {
+        return res.status(400).json({
+          error: 'API key is required',
+        });
+      }
+
+      await this.aiService.chat(req.body, res, apiKey);
+    } catch (error) {
+      next(error);
+    }
+  }
+}

+ 49 - 0
server/src/ai/ai.service.ts

@@ -0,0 +1,49 @@
+import OpenAI from 'openai';
+import { ChatRequestDto } from './dto/chat-request.dto';
+import { Response } from 'express';
+
+export class AIService {
+  private openai: OpenAI | null = null;
+
+  async chat(chatRequest: ChatRequestDto, res: Response, apiKey: string) {
+    try {
+      if (!apiKey) {
+        throw new Error('API key is required');
+      }
+
+      // Initialize OpenAI client only when needed
+      this.openai = new OpenAI({
+        apiKey,
+      });
+
+      const stream = await this.openai.chat.completions.create({
+        model: 'gpt-3.5-turbo',
+        messages: chatRequest.messages,
+        stream: true,
+      });
+
+      // Set headers for streaming response
+      res.setHeader('Content-Type', 'text/event-stream');
+      res.setHeader('Cache-Control', 'no-cache');
+      res.setHeader('Connection', 'keep-alive');
+
+      // Handle the stream
+      for await (const chunk of stream) {
+        const content = chunk.choices[0]?.delta?.content || '';
+        if (content) {
+          res.write(`data: ${JSON.stringify({ content })}\n\n`);
+        }
+      }
+
+      res.write('data: [DONE]\n\n');
+      res.end();
+    } catch (error) {
+      console.error('OpenAI API error:', error);
+      if (!res.headersSent) {
+        res.status(500).json({
+          error: error instanceof Error ? error.message : 'Failed to get response from OpenAI',
+        });
+      }
+    }
+  }
+}

+ 22 - 0
server/src/ai/dto.ts

@@ -0,0 +1,22 @@
+export interface MessageDto {
+  role: string;
+  content: string;
+}
+
+export interface ChatRequestDto {
+  messages: MessageDto[];
+}
+
+export function validateChatRequest(data: any): data is ChatRequestDto {
+  if (!data || !Array.isArray(data.messages)) {
+    return false;
+  }
+
+  return data.messages.every((message: any) => {
+    return (
+      message &&
+      typeof message.role === 'string' &&
+      typeof message.content === 'string'
+    );
+  });
+}

+ 6 - 0
server/src/ai/dto/chat-request.dto.ts

@@ -0,0 +1,6 @@
+export class ChatRequestDto {
+  messages: {
+    role: 'user' | 'assistant';
+    content: string;
+  }[];
+}

+ 4 - 0
server/src/ai/index.ts

@@ -0,0 +1,4 @@
+import { AIController } from './ai.controller';
+
+const aiController = new AIController();
+export const router = aiController.generateRoutes();

+ 2 - 0
server/src/app.ts

@@ -13,6 +13,7 @@ import { router as partitionsRouter } from './partitions';
 import { router as cronsRouter } from './crons';
 import { router as cronsRouter } from './crons';
 import { router as userRouter } from './users';
 import { router as userRouter } from './users';
 import { router as playgroundRouter } from './playground';
 import { router as playgroundRouter } from './playground';
+import { router as aiRouter } from './ai';
 import {
 import {
   TransformResMiddleware,
   TransformResMiddleware,
   LoggingMiddleware,
   LoggingMiddleware,
@@ -53,6 +54,7 @@ router.use('/partitions', partitionsRouter);
 router.use('/crons', cronsRouter);
 router.use('/crons', cronsRouter);
 router.use('/users', userRouter);
 router.use('/users', userRouter);
 router.use('/playground', playgroundRouter);
 router.use('/playground', playgroundRouter);
+router.use('/ai', aiRouter);
 router.get('/healthy', (req, res, next) => {
 router.get('/healthy', (req, res, next) => {
   res.json({ status: 200 });
   res.json({ status: 200 });
   next();
   next();

+ 5 - 0
server/yarn.lock

@@ -4590,6 +4590,11 @@ onetime@^5.1.0, onetime@^5.1.2:
   dependencies:
   dependencies:
     mimic-fn "^2.1.0"
     mimic-fn "^2.1.0"
 
 
+openai@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmjs.org/openai/-/openai-5.0.1.tgz#c7cabc4cb13554f8506158364566c16514d051fe"
+  integrity sha512-Do6vxhbDv7cXhji/4ct1lrpZYMAOmjYbhyA9LJTuG7OfpbWMpuS+EIXkRT7R+XxpRB1OZhU/op4FU3p3uxU6gw==
+
 ora@^5.1.0:
 ora@^5.1.0:
   version "5.4.1"
   version "5.4.1"
   resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
   resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"