feat: web components assistant (#422)

* chore: web components assistant

* chore: web components assistant

* docs: update notes
This commit is contained in:
BiggerRain
2025-04-23 18:23:40 +08:00
committed by GitHub
parent 9715a92f36
commit ee4a06b6de
18 changed files with 132 additions and 137 deletions

4
.env
View File

@@ -1,3 +1,3 @@
COCO_SERVER_URL=https://coco.infini.cloud #http://localhost:9000
COCO_SERVER_URL=http://localhost:9000 #https://coco.infini.cloud #http://localhost:9000
COCO_WEBSOCKET_URL=wss://coco.infini.cloud/ws #ws://localhost:9000/ws
COCO_WEBSOCKET_URL=ws://localhost:9000/ws #wss://coco.infini.cloud/ws #ws://localhost:9000/ws

View File

@@ -27,6 +27,7 @@ Information about release notes of Coco Server is provided here.
- feat: add support for AI assistant #394
- feat: add support for calculator function #399
- feat: auto selects the first item after searching #411
- feat: web components assistant #422
### Bug fix

File diff suppressed because one or more lines are too long

View File

@@ -12,12 +12,13 @@ import FontIcon from "@/components/Common/Icons/FontIcon";
import { useChatStore } from "@/stores/chatStore";
import { AI_ASSISTANT_PANEL_ID } from "@/constants";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { Get } from "@/api/axiosRequest";
interface AssistantListProps {
showChatHistory?: boolean;
assistantIDs?: string[];
}
export function AssistantList({ showChatHistory = true }: AssistantListProps) {
export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
const { t } = useTranslation();
const { connected } = useChatStore();
const isTauri = useAppStore((state) => state.isTauri);
@@ -35,17 +36,38 @@ export function AssistantList({ showChatHistory = true }: AssistantListProps) {
useClickAway(menuRef, () => setIsOpen(false));
const [assistants, setAssistants] = useState<any[]>([]);
const fetchAssistant = useCallback(async (serverId: string) => {
if (!isTauri) return;
const fetchAssistant = useCallback(async (serverId?: string) => {
let response: any;
if (isTauri) {
if (!serverId) return;
platformAdapter
.commands("assistant_search", {
try {
response = await platformAdapter.commands("assistant_search", {
serverId,
})
.then((res: any) => {
res = res ? JSON.parse(res) : null;
console.log("assistant_search", res);
const assistantList = res?.hits?.hits || [];
});
response = response ? JSON.parse(response) : null;
} catch (err) {
setAssistants([]);
setCurrentAssistant(null);
console.error("assistant_search", err);
}
} else {
const [error, res] = await Get(`/assistant/_search`);
if (error) {
setAssistants([]);
setCurrentAssistant(null);
console.error("assistant_search", error);
return;
}
console.log("/assistant/_search", res);
response = res;
}
console.log("assistant_search", response);
let assistantList = response?.hits?.hits || [];
assistantList = assistantIDs.length > 0
? assistantList.filter((item: any) => assistantIDs.includes(item._id))
: assistantList;
setAssistants(assistantList);
if (assistantList.length > 0) {
const assistant = assistantList.find(
@@ -57,12 +79,6 @@ export function AssistantList({ showChatHistory = true }: AssistantListProps) {
setCurrentAssistant(assistantList[0]);
}
}
})
.catch((err: any) => {
setAssistants([]);
setCurrentAssistant(null);
console.log("assistant_search", err);
});
}, []);
useEffect(() => {
@@ -98,7 +114,6 @@ export function AssistantList({ showChatHistory = true }: AssistantListProps) {
<div className="max-w-[100px] truncate">
{currentAssistant?._source?.name || "Coco AI"}
</div>
{showChatHistory && isTauri && (
<VisibleKey
aria-controls={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
shortcut={aiAssistant}
@@ -112,10 +127,9 @@ export function AssistantList({ showChatHistory = true }: AssistantListProps) {
}`}
/>
</VisibleKey>
)}
</button>
{showChatHistory && isTauri && isOpen && (
{isOpen && (
<div
id={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
className="absolute z-50 top-full mt-1 left-0 w-64 rounded-xl bg-white dark:bg-[#202126] p-2 text-sm/6 text-gray-800 dark:text-white shadow-lg border border-gray-200 dark:border-gray-700 focus:outline-none max-h-[calc(100vh-80px)] overflow-y-auto"
@@ -140,6 +154,7 @@ export function AssistantList({ showChatHistory = true }: AssistantListProps) {
<button
key={assistant._id}
onClick={() => {
console.log("assistant", assistant);
setCurrentAssistant(assistant);
setIsOpen(false);
}}

View File

@@ -35,6 +35,7 @@ interface ChatAIProps {
isChatPage?: boolean;
getFileUrl: (path: string) => string;
showChatHistory?: boolean;
assistantIDs?: string[];
}
export interface ChatAIRef {
@@ -58,6 +59,7 @@ const ChatAI = memo(
isChatPage = false,
getFileUrl,
showChatHistory,
assistantIDs,
},
ref
) => {
@@ -376,6 +378,7 @@ const ChatAI = memo(
isLogin={isLogin}
setIsLogin={setIsLogin}
showChatHistory={showChatHistory}
assistantIDs={assistantIDs}
/>
{isLogin ? (
<ChatContent

View File

@@ -1,6 +1,4 @@
import {
MessageSquarePlus,
} from "lucide-react";
import { MessageSquarePlus } from "lucide-react";
import clsx from "clsx";
import HistoryIcon from "@/icons/History";
@@ -27,6 +25,7 @@ interface ChatHeaderProps {
setIsLogin: (isLogin: boolean) => void;
isChatPage?: boolean;
showChatHistory?: boolean;
assistantIDs?: string[];
}
export function ChatHeader({
@@ -40,8 +39,8 @@ export function ChatHeader({
setIsLogin,
isChatPage = false,
showChatHistory = true,
assistantIDs,
}: ChatHeaderProps) {
const isPinned = useAppStore((state) => state.isPinned);
const setIsPinned = useAppStore((state) => state.setIsPinned);
@@ -94,7 +93,7 @@ export function ChatHeader({
</button>
)}
<AssistantList showChatHistory={showChatHistory} />
<AssistantList assistantIDs={assistantIDs} />
{showChatHistory ? (
<button

View File

@@ -1,9 +1,11 @@
import { useTranslation } from "react-i18next";
import { ChatMessage } from "@/components/ChatMessage";
import { useConnectStore } from "@/stores/connectStore";
export const Greetings = () => {
const { t } = useTranslation();
const currentAssistant = useConnectStore((state) => state.currentAssistant);
return (
<ChatMessage
@@ -12,7 +14,9 @@ export const Greetings = () => {
_id: "greetings",
_source: {
type: "assistant",
message: t("assistant.chat.greetings"),
message:
currentAssistant?._source?.chat_settings?.greeting_message ||
t("assistant.chat.greetings"),
},
}}
/>

View File

@@ -1,8 +1,7 @@
import { MoveRight } from "lucide-react";
import { FC, useEffect, useState } from "react";
import { Get } from "@/api/axiosRequest";
import { useAppStore } from "@/stores/appStore";
import { useConnectStore } from "@/stores/connectStore";
interface PrevSuggestionProps {
sendMessage: (message: string) => void;
@@ -11,35 +10,18 @@ interface PrevSuggestionProps {
const PrevSuggestion: FC<PrevSuggestionProps> = (props) => {
const { sendMessage } = props;
const isTauri = useAppStore((state) => state.isTauri);
const headersStr = localStorage.getItem("headers") || "{}";
const headers = JSON.parse(headersStr);
const id = headers["APP-INTEGRATION-ID"] || "cvkm9hmhpcemufsg3vug";
// console.log("id", id);
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const [list, setList] = useState<string[]>([]);
useEffect(() => {
if (!isTauri) getList();
}, [id]);
const getList = async () => {
if (!id) return;
const url = `/integration/${id}/chat/_suggest`;
const [error, res] = await Get(`/integration/${id}/chat/_suggest`);
if (error) {
console.error(url, error);
return setList([]);
const suggested = currentAssistant?._source?.chat_settings?.suggested || {};
if (suggested.enabled) {
setList(suggested.questions || []);
} else {
setList([]);
}
console.log("chat/_suggest", res);
setList(Array.isArray(res) ? res : []);
};
}, [JSON.stringify(currentAssistant)]);
return (
<ul className="absolute left-2 bottom-2 flex flex-col gap-2">

View File

@@ -15,7 +15,7 @@ import SearchPopover from "./SearchPopover";
// import AudioRecording from "../AudioRecording";
import { DataSource } from "@/types/commands";
// import InputExtra from "./InputExtra";
// import { useConnectStore } from "@/stores/connectStore";
import { useConnectStore } from "@/stores/connectStore";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import Copyright from "@/components/Common/Copyright";
import VisibleKey from "@/components/Common/VisibleKey";
@@ -55,7 +55,6 @@ interface ChatInputProps {
getFileMetadata: (path: string) => Promise<any>;
getFileIcon: (path: string, size: number) => Promise<string>;
hideCoco?: () => void;
hasFeature?: string[];
hasModules?: string[];
searchPlaceholder?: string;
chatPlaceholder?: string;
@@ -77,7 +76,6 @@ export default function ChatInput({
isChatPage = false,
getDataSourcesByServer,
setupWindowFocusListener,
hasFeature = ["think", "search", "think_active", "search_active"],
hideCoco,
hasModules = [],
searchPlaceholder,
@@ -85,6 +83,9 @@ export default function ChatInput({
}: ChatInputProps) {
const { t } = useTranslation();
const currentAssistant = useConnectStore((state) => state.currentAssistant);
console.log("currentAssistant", currentAssistant);
const showTooltip = useAppStore((state) => state.showTooltip);
const isPinned = useAppStore((state) => state.isPinned);
@@ -437,7 +438,7 @@ export default function ChatInput({
/>
)} */}
{hasFeature.includes("think") && (
{currentAssistant?._source?.config?.visible && (
<button
className={clsx(
"flex items-center gap-1 py-[3px] pl-1 pr-1.5 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
@@ -468,7 +469,7 @@ export default function ChatInput({
</button>
)}
{hasFeature.includes("search") && (
{currentAssistant?._source?.datasource?.visible && (
<SearchPopover
isSearchActive={isSearchActive}
setIsSearchActive={setIsSearchActive}
@@ -476,7 +477,7 @@ export default function ChatInput({
/>
)}
{!hasFeature.includes("search") && !hasFeature.includes("think") ? (
{!currentAssistant?._source?.datasource?.visible && !currentAssistant?._source?.config?.visible ? (
<div className="px-[9px]">
<Copyright />
</div>

View File

@@ -49,14 +49,16 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
>
<div
className={`${
showListRight ? "max-w-[450px] mobile:w-full" : "flex-1"
showListRight ? "max-w-[450px] mobile:max-w-full mobile:w-full" : "flex-1"
} min-w-0 flex gap-2 items-center justify-start `}
>
<ItemIcon item={item} />
<span className={`text-sm truncate text-left`}>{item?.title}</span>
</div>
{!isTauri && isMobile ? (
<div className="w-full text-xs truncate">{item?.summary}</div>
<div className="w-full text-xs text-gray-500 dark:text-gray-400 truncate">
{item?.summary}
</div>
) : null}
{showListRight && (isTauri || !isMobile) ? (
<ListRight

View File

@@ -24,13 +24,13 @@ import { useStartupStore } from "@/stores/startupStore";
import { DataSource } from "@/types/commands";
import { useThemeStore } from "@/stores/themeStore";
import { Get } from "@/api/axiosRequest";
import { useConnectStore } from "@/stores/connectStore";
interface SearchChatProps {
isTauri?: boolean;
hasModules?: string[];
defaultModule?: "search" | "chat";
hasFeature?: string[];
showChatHistory?: boolean;
theme?: "auto" | "light" | "dark";
@@ -41,13 +41,13 @@ interface SearchChatProps {
setIsPinned?: (value: boolean) => void;
onModeChange?: (isChatMode: boolean) => void;
isMobile?: boolean;
assistantIDs?: string[];
}
function SearchChat({
isTauri = true,
hasModules = ["search", "chat"],
defaultModule = "search",
hasFeature = ["think", "search", "think_active", "search_active"],
theme,
hideCoco,
searchPlaceholder,
@@ -56,11 +56,14 @@ function SearchChat({
setIsPinned,
onModeChange,
isMobile = false,
assistantIDs,
}: SearchChatProps) {
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const customInitialState = {
...initialAppState,
isDeepThinkActive: hasFeature.includes("think_active"),
isSearchActive: hasFeature.includes("search_active"),
isDeepThinkActive: currentAssistant?._source?.type === "deep_think",
isSearchActive: currentAssistant?._source?.datasource?.enabled === true,
};
const [state, dispatch] = useReducer(appReducer, customInitialState);
@@ -172,26 +175,31 @@ function SearchChat({
query?: string;
}
): Promise<DataSource[]> => {
let response: any;
if (isTauri) {
return platformAdapter.invokeBackend("get_datasources_by_server", {
response = platformAdapter.invokeBackend("get_datasources_by_server", {
id: serverId,
options,
});
} else {
const [error, response]: any = await Get("/datasource/_search");
const [error, res]: any = await Get("/datasource/_search");
if (error) {
console.error("_search", error);
return [];
}
const res = response?.hits?.hits?.map((item: any) => {
response = res?.hits?.hits?.map((item: any) => {
return {
...item,
id: item._source.id,
name: item._source.name,
};
});
return res || [];
}
let ids = currentAssistant?._source?.datasource?.ids;
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
response = response.filter((item: any) => ids.includes(item.id));
}
return response || [];
},
[]
);
@@ -300,7 +308,6 @@ function SearchChat({
setIsSearchActive={toggleSearchActive}
isDeepThinkActive={isDeepThinkActive}
setIsDeepThinkActive={toggleDeepThinkActive}
hasFeature={hasFeature}
getDataSourcesByServer={getDataSourcesByServer}
setupWindowFocusListener={setupWindowFocusListener}
checkScreenPermission={checkScreenPermission}
@@ -356,6 +363,7 @@ function SearchChat({
isDeepThinkActive={isDeepThinkActive}
getFileUrl={getFileUrl}
showChatHistory={showChatHistory}
assistantIDs={assistantIDs}
/>
</Suspense>
</div>

View File

@@ -30,7 +30,6 @@ import {
import { DataSource } from "@/types/commands";
import HistoryList from "@/components/Common/HistoryList";
import { useSyncStore } from "@/hooks/useSyncStore";
import { useFeatureControl } from "@/hooks/useFeatureControl";
interface ChatProps {}
@@ -262,12 +261,6 @@ export default function Chat({}: ChatProps) {
await delete_session_chat(currentService.id, id);
};
const hasFeature = useFeatureControl({
initialFeatures: ["think", "search"],
featureToToggle: "think",
condition: (item) => item?._source?.type === "simple"
});
return (
<div className="h-screen">
<div className="h-full flex">
@@ -337,7 +330,6 @@ export default function Chat({}: ChatProps) {
openFileDialog={openFileDialog}
getFileMetadata={getFileMetadata}
getFileIcon={getFileIcon}
hasFeature={hasFeature}
/>
</div>
</div>

View File

@@ -4,7 +4,6 @@ import SearchChat from "@/components/SearchChat";
import platformAdapter from "@/utils/platformAdapter";
import { useAppStore } from "@/stores/appStore";
import { useSyncStore } from "@/hooks/useSyncStore";
import { useFeatureControl } from "@/hooks/useFeatureControl";
function MainApp() {
const setIsTauri = useAppStore((state) => state.setIsTauri);
@@ -16,18 +15,11 @@ function MainApp() {
useSyncStore();
const hasFeature = useFeatureControl({
initialFeatures: ["think", "search"],
featureToToggle: "think",
condition: (item) => item?._source?.type === "simple",
});
return (
<SearchChat
isTauri={true}
hideCoco={hideCoco}
hasModules={["search", "chat"]}
hasFeature={hasFeature}
/>
);
}

View File

@@ -32,12 +32,6 @@
- **默认值**: `['search', 'chat']`
- **描述**: 启用的功能模块列表,目前支持 'search' 和 'chat' 模块
### `hasFeature`
- **类型**: `string[]`
- **可选**: 是
- **默认值**: `['think', 'search', 'think_active', 'search_active']`
- **描述**: 启用的特性列表,支持 'think'、'search'、'think_active'、'search_active' 特性。其中 'think_active' 表示默认开启深度思考,'search_active' 表示默认开启搜索
### `hideCoco`
- **类型**: `() => void`
- **可选**: 是
@@ -87,7 +81,6 @@ function App() {
width={680}
height={590}
hasModules={['search', 'chat']}
hasFeature={['think', 'search', 'think_active', 'search_active']}
hideCoco={() => console.log('hide')}
theme="dark"
searchPlaceholder=""

View File

@@ -5,7 +5,6 @@ import { useAppStore } from "@/stores/appStore";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
import { useFeatureControl } from "@/hooks/useFeatureControl";
import "@/i18n";
import "@/web.css";
@@ -17,7 +16,7 @@ interface WebAppProps {
height?: number;
hasModules?: string[];
defaultModule?: "search" | "chat";
hasFeature?: string[];
assistantIDs?: string[];
hideCoco?: () => void;
theme?: "auto" | "light" | "dark";
searchPlaceholder?: string;
@@ -32,7 +31,7 @@ function WebApp({
height = 590,
headers = {
"X-API-TOKEN":
"cvvitp6hpceh0ip1q1706byts41c7213k4el22v3bp6f4ta2sar0u29jp4pg08h6xcyxn085x3lq1k7wojof",
"cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh",
"APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug",
},
// token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n", // https://coco.infini.cloud
@@ -42,7 +41,7 @@ function WebApp({
hideCoco = () => {},
hasModules = ["search", "chat"],
defaultModule = "search",
hasFeature = ["think_active", "search_active"],
assistantIDs = [],
theme = "dark",
searchPlaceholder = "",
chatPlaceholder = "",
@@ -71,12 +70,6 @@ function WebApp({
const [isChatMode, setIsChatMode] = useState(false);
const hasFeatureCopy = useFeatureControl({
initialFeatures: hasFeature,
featureToToggle: "think",
condition: (item) => item?._source?.type === "simple",
});
return (
<div
id="searchChat-container"
@@ -91,7 +84,7 @@ function WebApp({
{isMobile && (
<div
className={`fixed ${
isChatMode ? "top-2" : "top-3"
isChatMode ? "top-1" : "top-3"
} right-2 flex items-center justify-center w-8 h-8 rounded-full bg-black/10 dark:bg-white/10 cursor-pointer z-50`}
onClick={onCancel}
>
@@ -110,7 +103,6 @@ function WebApp({
hideCoco={hideCoco}
hasModules={hasModules}
defaultModule={defaultModule}
hasFeature={hasFeatureCopy}
theme={theme}
searchPlaceholder={searchPlaceholder}
chatPlaceholder={chatPlaceholder}
@@ -118,6 +110,7 @@ function WebApp({
setIsPinned={setIsPinned}
onModeChange={setIsChatMode}
isMobile={isMobile}
assistantIDs={assistantIDs}
/>
</div>
);

View File

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

View File

@@ -67,7 +67,7 @@ export default defineConfig({
const packageJson = {
name: "@infinilabs/search-chat",
version: "1.1.4",
version: "1.1.5",
main: "index.js",
module: "index.js",
type: "module",

View File

@@ -61,6 +61,16 @@ export default defineConfig(async () => ({
changeOrigin: true,
secure: false,
},
"/assistant": {
target: process.env.COCO_SERVER_URL,
changeOrigin: true,
secure: false,
},
"/datasource": {
target: process.env.COCO_SERVER_URL,
changeOrigin: true,
secure: false,
},
},
},
build: {