feat: add chat mode launch page (#424)

This commit is contained in:
ayangweb
2025-04-23 18:50:32 +08:00
committed by GitHub
parent 4380b56a30
commit bde658b981
13 changed files with 271 additions and 51 deletions

View File

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

View File

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

View File

@@ -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())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

@@ -131,6 +131,9 @@ export default function ChatInput({
const setModifierKeyPressed = useShortcutsStore((state) => { const setModifierKeyPressed = useShortcutsStore((state) => {
return state.setModifierKeyPressed; return state.setModifierKeyPressed;
}); });
const setVisibleStartPage = useConnectStore((state) => {
return state.setVisibleStartPage;
});
useEffect(() => { useEffect(() => {
const handleFocus = () => { const handleFocus = () => {
@@ -154,6 +157,8 @@ export default function ChatInput({
}, [isChatMode, textareaRef, inputRef]); }, [isChatMode, textareaRef, inputRef]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
setVisibleStartPage(false);
const trimmedValue = inputValue.trim(); const trimmedValue = inputValue.trim();
console.log("handleSubmit", trimmedValue, disabled); console.log("handleSubmit", trimmedValue, disabled);
if (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]"> <div className="px-[9px]">
<Copyright /> <Copyright />
</div> </div>

View File

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

View File

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

View File

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