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,
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();

View File

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

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 { 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;
};

View File

@@ -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={() => {

View File

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

View File

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

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 { 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 ? (

View File

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

View File

@@ -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",

View File

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

View File

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