Bladeren bron

feat: add chat mode launch page (#424)

ayangweb 1 maand geleden
bovenliggende
commit
bde658b981

+ 2 - 1
src-tauri/src/lib.rs

@@ -135,7 +135,8 @@ pub fn run() {
             server::transcription::transcription,
             local::application::get_default_search_paths,
             local::application::list_app_with_metadata_in,
-            util::open
+            util::open,
+            server::system_settings::get_system_settings
         ])
         .setup(|app| {
             let registry = SearchSourceRegistry::default();

+ 1 - 0
src-tauri/src/server/mod.rs

@@ -8,5 +8,6 @@ pub mod http_client;
 pub mod profile;
 pub mod search;
 pub mod servers;
+pub mod system_settings;
 pub mod transcription;
 pub mod websocket;

+ 15 - 0
src-tauri/src/server/system_settings.rs

@@ -0,0 +1,15 @@
+use crate::server::http_client::HttpClient;
+use serde_json::Value;
+use tauri::command;
+
+#[command]
+pub async fn get_system_settings(server_id: String) -> Result<Value, String> {
+    let response = HttpClient::get(&server_id, "/settings", None)
+        .await
+        .map_err(|err| err.to_string())?;
+
+    response
+        .json::<Value>()
+        .await
+        .map_err(|err| err.to_string())
+}

+ 9 - 8
src/api/axiosRequest.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 
-import { useAppStore } from '@/stores/appStore';
+import { useAppStore } from "@/stores/appStore";
 
 import {
   handleChangeRequestHeader,
@@ -44,21 +44,22 @@ axios.interceptors.response.use(
 
 export const handleApiError = (error: any) => {
   const addError = useAppStore.getState().addError;
-  
-  let message = 'Request failed';
-  
+
+  let message = "Request failed";
+
   if (error.response) {
     // Server error response
-    message = error.response.data?.message || `Error (${error.response.status})`;
+    message =
+      error.response.data?.message || `Error (${error.response.status})`;
   } else if (error.request) {
     // Request failed to send
-    message = 'Network connection failed';
+    message = "Network connection failed";
   } else {
     // Other errors
     message = error.message;
   }
-  
-  addError(message, 'error');
+
+  addError(message, "error");
   return error;
 };
 

+ 10 - 8
src/components/Assistant/AssistantList.tsx

@@ -22,6 +22,8 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
   const { t } = useTranslation();
   const { connected } = useChatStore();
   const isTauri = useAppStore((state) => state.isTauri);
+  const assistantList = useConnectStore((state) => state.assistantList);
+  const setAssistantList = useConnectStore((state) => state.setAssistantList);
   const currentService = useConnectStore((state) => state.currentService);
   const currentAssistant = useConnectStore((state) => state.currentAssistant);
   const setCurrentAssistant = useConnectStore(
@@ -34,7 +36,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
   const menuRef = useRef<HTMLDivElement>(null);
 
   useClickAway(menuRef, () => setIsOpen(false));
-  const [assistants, setAssistants] = useState<any[]>([]);
 
   const fetchAssistant = useCallback(async (serverId?: string) => {
     let response: any;
@@ -46,14 +47,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
         });
         response = response ? JSON.parse(response) : null;
       } catch (err) {
-        setAssistants([]);
+        setAssistantList([]);
         setCurrentAssistant(null);
         console.error("assistant_search", err);
       }
     } else {
       const [error, res] = await Get(`/assistant/_search`);
       if (error) {
-        setAssistants([]);
+        setAssistantList([]);
         setCurrentAssistant(null);
         console.error("assistant_search", error);
         return;
@@ -64,11 +65,12 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
     console.log("assistant_search", response);
     let assistantList = response?.hits?.hits || [];
 
-    assistantList = assistantIDs.length > 0
-      ? assistantList.filter((item: any) => assistantIDs.includes(item._id))
-      : assistantList;
+    assistantList =
+      assistantIDs.length > 0
+        ? assistantList.filter((item: any) => assistantIDs.includes(item._id))
+        : assistantList;
 
-    setAssistants(assistantList);
+    setAssistantList(assistantList);
     if (assistantList.length > 0) {
       const assistant = assistantList.find(
         (item: any) => item._id === currentAssistant?._id
@@ -150,7 +152,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
               </VisibleKey>
             </button>
           </div>
-          {assistants.map((assistant) => (
+          {assistantList.map((assistant) => (
             <button
               key={assistant._id}
               onClick={() => {

+ 6 - 1
src/components/Assistant/Chat.tsx

@@ -74,6 +74,9 @@ const ChatAI = memo(
         useChatStore();
 
       const currentService = useConnectStore((state) => state.currentService);
+      const visibleStartPage = useConnectStore((state) => {
+        return state.visibleStartPage;
+      });
 
       const addError = useAppStore.getState().addError;
 
@@ -402,7 +405,9 @@ const ChatAI = memo(
             <ConnectPrompt />
           )}
 
-          {showPrevSuggestion ? <PrevSuggestion sendMessage={init} /> : null}
+          {showPrevSuggestion && !visibleStartPage && (
+            <PrevSuggestion sendMessage={init} />
+          )}
         </div>
       );
     }

+ 3 - 0
src/components/Assistant/ChatContent.tsx

@@ -10,6 +10,7 @@ import type { Chat, IChunkData } from "./types";
 // import SessionFile from "./SessionFile";
 import { useConnectStore } from "@/stores/connectStore";
 import SessionFile from "./SessionFile";
+import Splash from "./Splash";
 
 interface ChatContentProps {
   activeChat?: Chat;
@@ -145,6 +146,8 @@ export const ChatContent = ({
       )}
 
       {sessionId && <SessionFile sessionId={sessionId} />}
+
+      <Splash />
     </div>
   );
 };

+ 159 - 0
src/components/Assistant/Splash.tsx

@@ -0,0 +1,159 @@
+import { CircleX, MoveRight } from "lucide-react";
+import { useMount } from "ahooks";
+import { useAppStore } from "@/stores/appStore";
+import { useMemo, useState } from "react";
+import platformAdapter from "@/utils/platformAdapter";
+import { useConnectStore } from "@/stores/connectStore";
+import { useThemeStore } from "@/stores/themeStore";
+import FontIcon from "../Common/Icons/FontIcon";
+import logoImg from "@/assets/icon.svg";
+import { Get } from "@/api/axiosRequest";
+
+interface StartPage {
+  enabled?: boolean;
+  logo?: {
+    light?: string;
+    dark?: string;
+  };
+  introduction?: string;
+  display_assistants?: string[];
+}
+
+export interface Response {
+  app_settings?: {
+    chat?: {
+      start_page?: StartPage;
+    };
+  };
+}
+
+const Splash = () => {
+  const isTauri = useAppStore((state) => state.isTauri);
+  const currentService = useConnectStore((state) => state.currentService);
+  const [settings, setSettings] = useState<StartPage>();
+  const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
+  const setVisibleStartPage = useConnectStore((state) => {
+    return state.setVisibleStartPage;
+  });
+  const addError = useAppStore((state) => state.addError);
+  const isDark = useThemeStore((state) => state.isDark);
+  const assistantList = useConnectStore((state) => state.assistantList);
+  const setCurrentAssistant = useConnectStore((state) => {
+    return state.setCurrentAssistant;
+  });
+
+  useMount(async () => {
+    try {
+      const serverId = currentService.id;
+
+      let response: Response = {};
+
+      if (isTauri) {
+        response = await platformAdapter.invokeBackend<Response>(
+          "get_system_settings",
+          {
+            serverId,
+          }
+        );
+      } else {
+        const [err, result] = await Get("/settings");
+
+        if (err) {
+          throw new Error(err);
+        }
+
+        response = result as Response;
+      }
+
+      const settings = response?.app_settings?.chat?.start_page;
+
+      setVisibleStartPage(Boolean(settings?.enabled));
+
+      setSettings(settings);
+    } catch (error) {
+      addError(String(error), "error");
+    }
+  });
+
+  const settingsAssistantList = useMemo(() => {
+    console.log("assistantList", assistantList);
+
+    return assistantList.filter((item) => {
+      return settings?.display_assistants?.includes(item?._source?.id);
+    });
+  }, [settings, assistantList]);
+
+  const logo = useMemo(() => {
+    const { light, dark } = settings?.logo || {};
+
+    if (isDark) {
+      return dark || light;
+    }
+
+    return light || dark;
+  }, [settings, isDark]);
+
+  return (
+    visibleStartPage && (
+      <div className="absolute inset-0 flex flex-col items-center px-6 pt-6 text-[#333] dark:text-white">
+        <CircleX
+          className="absolute top-3 right-3 size-4 text-[#999] cursor-pointer"
+          onClick={() => {
+            setVisibleStartPage(false);
+          }}
+        />
+
+        <img src={logo} className="h-8" />
+
+        <div className="mt-3 mb-6 text-lg font-medium">
+          {settings?.introduction}
+        </div>
+
+        <ul className="flex flex-wrap -m-1">
+          {settingsAssistantList?.map((item) => {
+            const { id, name, description, icon } = item._source;
+
+            return (
+              <li key={id} className="w-1/2 p-1">
+                <div
+                  className="group h-[74px] px-3 py-2 text-sm rounded-xl border dark:border-[#262626] bg-white dark:bg-black cursor-pointer transition hover:!border-[#0087FF]"
+                  onClick={() => {
+                    setCurrentAssistant(item);
+
+                    setVisibleStartPage(false);
+                  }}
+                >
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-1">
+                      {icon?.startsWith("font_") ? (
+                        <div className="size-4 flex items-center justify-center rounded-full bg-white">
+                          <FontIcon name={icon} className="w-5 h-5" />
+                        </div>
+                      ) : (
+                        <img
+                          src={logoImg}
+                          className="size-4 rounded-full"
+                          alt={name}
+                        />
+                      )}
+
+                      <span>{name}</span>
+                    </div>
+
+                    <MoveRight className="size-4 transition group-hover:text-[#0087FF]" />
+                  </div>
+
+                  <div className="mt-1 text-xs text-[#999] line-clamp-2">
+                    {description}
+                  </div>
+                </div>
+              </li>
+            );
+          })}
+        </ul>
+      </div>
+    )
+  );
+};
+
+export default Splash;

+ 12 - 2
src/components/ChatMessage/index.tsx

@@ -14,6 +14,7 @@ import { SuggestionList } from "./SuggestionList";
 import { UserMessage } from "./UserMessage";
 import { useConnectStore } from "@/stores/connectStore";
 import FontIcon from "@/components/Common/Icons/FontIcon";
+import clsx from "clsx";
 
 interface ChatMessageProps {
   message: Message;
@@ -53,6 +54,7 @@ export const ChatMessage = memo(function ChatMessage({
     isTyping === false && (messageContent || response?.message_chunk);
 
   const [suggestion, setSuggestion] = useState<string[]>([]);
+  const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
 
   const getSuggestion = (suggestion: string[]) => {
     setSuggestion(suggestion);
@@ -121,7 +123,13 @@ export const ChatMessage = memo(function ChatMessage({
 
   return (
     <div
-      className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
+      className={clsx(
+        "py-8 flex",
+        [isAssistant ? "justify-start" : "justify-end"],
+        {
+          hidden: visibleStartPage,
+        }
+      )}
     >
       <div
         className={`px-4 flex gap-4 ${
@@ -129,7 +137,9 @@ export const ChatMessage = memo(function ChatMessage({
         }`}
       >
         <div
-          className={`w-full space-y-2 ${isAssistant ? "text-left" : "text-right"}`}
+          className={`w-full space-y-2 ${
+            isAssistant ? "text-left" : "text-right"
+          }`}
         >
           <div className="w-full flex items-center gap-1 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
             {isAssistant ? (

+ 7 - 1
src/components/Search/InputBox.tsx

@@ -131,6 +131,9 @@ export default function ChatInput({
   const setModifierKeyPressed = useShortcutsStore((state) => {
     return state.setModifierKeyPressed;
   });
+  const setVisibleStartPage = useConnectStore((state) => {
+    return state.setVisibleStartPage;
+  });
 
   useEffect(() => {
     const handleFocus = () => {
@@ -154,6 +157,8 @@ export default function ChatInput({
   }, [isChatMode, textareaRef, inputRef]);
 
   const handleSubmit = useCallback(() => {
+    setVisibleStartPage(false);
+
     const trimmedValue = inputValue.trim();
     console.log("handleSubmit", trimmedValue, disabled);
     if (trimmedValue && !disabled) {
@@ -477,7 +482,8 @@ export default function ChatInput({
               />
             )}
 
-            {!currentAssistant?._source?.datasource?.visible && !currentAssistant?._source?.config?.visible ? (
+            {!currentAssistant?._source?.datasource?.visible &&
+            !currentAssistant?._source?.config?.visible ? (
               <div className="px-[9px]">
                 <Copyright />
               </div>

+ 12 - 0
src/stores/connectStore.ts

@@ -24,10 +24,14 @@ export type IConnectStore = {
   setConnectionTimeout: (connectionTimeout: number) => void;
   currentSessionId?: string;
   setCurrentSessionId: (currentSessionId?: string) => void;
+  assistantList: any[];
+  setAssistantList: (assistantList: []) => void;
   currentAssistant: any;
   setCurrentAssistant: (assistant: any) => void;
   queryTimeout: number;
   setQueryTimeout: (queryTimeout: number) => void;
+  visibleStartPage: boolean;
+  setVisibleStartPage: (visibleStartPage: boolean) => void;
 };
 
 export const useConnectStore = create<IConnectStore>()(
@@ -91,6 +95,10 @@ export const useConnectStore = create<IConnectStore>()(
         setCurrentSessionId(currentSessionId) {
           return set(() => ({ currentSessionId }));
         },
+        assistantList: [],
+        setAssistantList: (assistantList) => {
+          return set(() => ({ assistantList }));
+        },
         currentAssistant: null,
         setCurrentAssistant: (assistant: any) => {
           set(
@@ -103,6 +111,10 @@ export const useConnectStore = create<IConnectStore>()(
         setQueryTimeout: (queryTimeout: number) => {
           return set(() => ({ queryTimeout }));
         },
+        visibleStartPage: false,
+        setVisibleStartPage: (visibleStartPage: boolean) => {
+          return set(() => ({ visibleStartPage }));
+        },
       }),
       {
         name: "connect-store",

+ 4 - 4
src/utils/platformAdapter.ts

@@ -1,8 +1,8 @@
 // manual modification
-import { createWebAdapter } from './webAdapter';
-//import { createTauriAdapter } from "./tauriAdapter";
+import { createTauriAdapter } from "./tauriAdapter";
+// import { createWebAdapter } from './webAdapter';
 
-//let platformAdapter = createTauriAdapter();
-let platformAdapter = createWebAdapter();
+let platformAdapter = createTauriAdapter();
+// let platformAdapter = createWebAdapter();
 
 export default platformAdapter;

+ 31 - 26
vite.config.ts

@@ -1,8 +1,8 @@
 import { defineConfig } from "vite";
 import react from "@vitejs/plugin-react";
-import path from 'path';
+import path from "path";
 import { config } from "dotenv";
-import packageJson from './package.json';
+import packageJson from "./package.json";
 
 config();
 
@@ -12,12 +12,12 @@ const host = process.env.TAURI_DEV_HOST;
 // https://vitejs.dev/config/
 export default defineConfig(async () => ({
   define: {
-    'process.env.VERSION': JSON.stringify(packageJson.version),
+    "process.env.VERSION": JSON.stringify(packageJson.version),
   },
   plugins: [react()],
   resolve: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      "@": path.resolve(__dirname, "./src"),
     },
   },
   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
@@ -31,10 +31,10 @@ export default defineConfig(async () => ({
     host: host || false,
     hmr: host
       ? {
-        protocol: "ws",
-        host,
-        port: 1421,
-      }
+          protocol: "ws",
+          host,
+          port: 1421,
+        }
       : undefined,
     watch: {
       // 3. tell vite to ignore watching `src-tauri`
@@ -71,31 +71,36 @@ export default defineConfig(async () => ({
         changeOrigin: true,
         secure: false,
       },
+      "/settings": {
+        target: process.env.COCO_SERVER_URL,
+        changeOrigin: true,
+        secure: false,
+      },
     },
   },
   build: {
     rollupOptions: {
       output: {
         manualChunks: {
-          vendor: ['react', 'react-dom'],
-          katex: ['rehype-katex'],
-          highlight: ['rehype-highlight'],
-          mermaid: ['mermaid'],
-          'tauri-api': [
-            '@tauri-apps/api/core',
-            '@tauri-apps/api/event',
-            '@tauri-apps/api/window',
-            '@tauri-apps/api/dpi',
-            '@tauri-apps/api/webviewWindow'
+          vendor: ["react", "react-dom"],
+          katex: ["rehype-katex"],
+          highlight: ["rehype-highlight"],
+          mermaid: ["mermaid"],
+          "tauri-api": [
+            "@tauri-apps/api/core",
+            "@tauri-apps/api/event",
+            "@tauri-apps/api/window",
+            "@tauri-apps/api/dpi",
+            "@tauri-apps/api/webviewWindow",
           ],
-          'tauri-plugins': [
-            '@tauri-apps/plugin-dialog',
-            '@tauri-apps/plugin-process',
-            'tauri-plugin-fs-pro-api',
-            'tauri-plugin-macos-permissions-api',
-            'tauri-plugin-screenshots-api',
-          ]
-        }
+          "tauri-plugins": [
+            "@tauri-apps/plugin-dialog",
+            "@tauri-apps/plugin-process",
+            "tauri-plugin-fs-pro-api",
+            "tauri-plugin-macos-permissions-api",
+            "tauri-plugin-screenshots-api",
+          ],
+        },
       },
     },
     chunkSizeWarningLimit: 600,