mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: add chat mode launch page (#424)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
src-tauri/src/server/system_settings.rs
Normal file
15
src-tauri/src/server/system_settings.rs
Normal 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())
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
src/components/Assistant/Splash.tsx
Normal file
159
src/components/Assistant/Splash.tsx
Normal 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;
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user