mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-23 14:59:24 +01:00
chore: search-chat components add formatUrl & think data & icons url (#765)
* chore: web components add formatUrl & think data * chore: add headers * chore: add * chhore: add server url * docs: update notes * chore: url * docs: search chat docs
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -32,6 +32,7 @@
|
||||
"localstorage",
|
||||
"lucide",
|
||||
"maximizable",
|
||||
"mdast",
|
||||
"meval",
|
||||
"Minimizable",
|
||||
"msvc",
|
||||
|
||||
@@ -37,6 +37,7 @@ Information about release notes of Coco Server is provided here.
|
||||
- chore: rename QuickLink/quick_link to Quicklink/quicklink #752
|
||||
- chore: assistant params & styles #753
|
||||
- chore: make optional fields optional #758
|
||||
- chore: search-chat components add formatUrl & think data & icons url #765
|
||||
|
||||
## 0.6.0 (2025-06-29)
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export const Get = <T>(
|
||||
|
||||
export const Post = <T>(
|
||||
url: string,
|
||||
data: IAnyObj,
|
||||
data: IAnyObj | undefined,
|
||||
params: IAnyObj = {},
|
||||
headers: IAnyObj = {}
|
||||
): Promise<[any, FcResponse<T> | undefined]> => {
|
||||
|
||||
@@ -15,11 +15,14 @@ export async function streamPost({
|
||||
}) {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
|
||||
let baseURL = appStore.state?.endpoint_http
|
||||
let baseURL = appStore.state?.endpoint_http;
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
baseURL = "";
|
||||
}
|
||||
|
||||
const headersStr = localStorage.getItem("headers") || "{}";
|
||||
const headersStorage = JSON.parse(headersStr);
|
||||
|
||||
const query = new URLSearchParams(queryParams || {}).toString();
|
||||
const fullUrl = `${baseURL}${url}?${query}`;
|
||||
|
||||
@@ -28,6 +31,7 @@ export async function streamPost({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(headersStorage),
|
||||
...(headers || {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
import { clientEnv } from "@/utils/env";
|
||||
import { useLogStore } from "@/stores/logStore";
|
||||
import { get_server_token } from "@/commands";
|
||||
interface FetchRequestConfig {
|
||||
url: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
parseAs?: "json" | "text" | "binary";
|
||||
baseURL?: string;
|
||||
}
|
||||
|
||||
interface FetchResponse<T = any> {
|
||||
data: T;
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
const timeoutPromise = (ms: number) => {
|
||||
return new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Request timed out after ${ms} ms`)), ms)
|
||||
);
|
||||
};
|
||||
|
||||
export const tauriFetch = async <T = any>({
|
||||
url,
|
||||
method = "GET",
|
||||
headers = {},
|
||||
body,
|
||||
timeout = 30,
|
||||
parseAs = "json",
|
||||
baseURL = clientEnv.COCO_SERVER_URL
|
||||
}: FetchRequestConfig): Promise<FetchResponse<T>> => {
|
||||
const addLog = useLogStore.getState().addLog;
|
||||
|
||||
try {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
const connectStore = JSON.parse(localStorage.getItem("connect-store") || "{}");
|
||||
console.log("baseURL", appStore.state?.endpoint_http)
|
||||
|
||||
baseURL = appStore.state?.endpoint_http || baseURL;
|
||||
|
||||
const authStore = JSON.parse(localStorage.getItem("auth-store") || "{}")
|
||||
const auth = authStore?.state?.auth
|
||||
console.log("auth", auth)
|
||||
|
||||
if (baseURL.endsWith("/")) {
|
||||
baseURL = baseURL.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
// If not, prepend the defaultPrefix
|
||||
url = baseURL + url;
|
||||
}
|
||||
|
||||
if (method !== "GET") {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const server_id = connectStore.state?.currentService?.id || "default_coco_server"
|
||||
const res: any = await get_server_token(server_id);
|
||||
|
||||
headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || res?.access_token || undefined;
|
||||
|
||||
// debug API
|
||||
const requestInfo = {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
timeout,
|
||||
parseAs,
|
||||
};
|
||||
|
||||
const fetchPromise = fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const response = await Promise.race([
|
||||
fetchPromise,
|
||||
timeoutPromise(timeout * 1000),
|
||||
]);
|
||||
|
||||
const statusText = response.ok ? "OK" : "Error";
|
||||
|
||||
let data: any;
|
||||
if (parseAs === "json") {
|
||||
data = await response.json();
|
||||
} else if (parseAs === "text") {
|
||||
data = await response.text();
|
||||
} else {
|
||||
data = await response.arrayBuffer();
|
||||
}
|
||||
|
||||
// debug API
|
||||
const log = {
|
||||
request: requestInfo,
|
||||
response: {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText,
|
||||
headers: response.headers,
|
||||
},
|
||||
};
|
||||
addLog(log);
|
||||
|
||||
return log.response;
|
||||
} catch (error) {
|
||||
console.error("Request failed:", error);
|
||||
|
||||
// debug API
|
||||
const log = {
|
||||
request: {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
timeout,
|
||||
parseAs,
|
||||
},
|
||||
error,
|
||||
};
|
||||
addLog(log);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
1
src/assets/assets/fonts/icons/iconfont-app.js
Normal file
1
src/assets/assets/fonts/icons/iconfont-app.js
Normal file
File diff suppressed because one or more lines are too long
1
src/assets/assets/fonts/icons/iconfont.js
Normal file
1
src/assets/assets/fonts/icons/iconfont.js
Normal file
File diff suppressed because one or more lines are too long
@@ -28,7 +28,11 @@ interface State {
|
||||
activeMenuIndex: number;
|
||||
}
|
||||
|
||||
const ContextMenu = () => {
|
||||
interface ContextMenuProps {
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { t, i18n } = useTranslation();
|
||||
const state = useReactive<State>({
|
||||
@@ -122,7 +126,10 @@ const ContextMenu = () => {
|
||||
shortcut: "enter",
|
||||
hide: category === "Calculator",
|
||||
clickEvent: () => {
|
||||
platformAdapter.openSearchItem(selectedSearchContent as any);
|
||||
platformAdapter.openSearchItem(
|
||||
selectedSearchContent as any,
|
||||
formatUrl
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -135,7 +142,7 @@ const ContextMenu = () => {
|
||||
type === "AI Assistant" ||
|
||||
id === "Extension Store",
|
||||
clickEvent() {
|
||||
copyToClipboard(url);
|
||||
copyToClipboard(formatUrl && formatUrl(selectedSearchContent) || url);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ interface DocumentListProps {
|
||||
selectedId?: string;
|
||||
viewMode: "detail" | "list";
|
||||
setViewMode: (mode: "detail" | "list") => void;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -30,6 +31,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
isChatMode,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
formatUrl,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
@@ -174,7 +176,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
if (selectedItem === null) return;
|
||||
const item = data.list[selectedItem]?.document;
|
||||
|
||||
platformAdapter.openSearchItem(item);
|
||||
platformAdapter.openSearchItem(item, formatUrl);
|
||||
};
|
||||
|
||||
switch (e.key) {
|
||||
@@ -238,7 +240,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
currentIndex={index}
|
||||
onMouseEnter={() => onMouseEnter(index, hit.document)}
|
||||
onItemClick={() => {
|
||||
platformAdapter.openSearchItem(hit.document);
|
||||
platformAdapter.openSearchItem(hit.document, formatUrl);
|
||||
}}
|
||||
showListRight={viewMode === "list"}
|
||||
/>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface DropdownListProps {
|
||||
isSearchComplete: boolean;
|
||||
isChatMode: boolean;
|
||||
globalItemIndexMap: Record<number, SearchDocument>;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
function DropdownList({
|
||||
@@ -34,6 +35,7 @@ function DropdownList({
|
||||
isError,
|
||||
isChatMode,
|
||||
globalItemIndexMap,
|
||||
formatUrl,
|
||||
}: DropdownListProps) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -77,7 +79,7 @@ function DropdownList({
|
||||
setSelectedSearchContent(item);
|
||||
},
|
||||
onItemClick: (item: SearchDocument) => {
|
||||
platformAdapter.openSearchItem(item);
|
||||
platformAdapter.openSearchItem(item, formatUrl);
|
||||
},
|
||||
goToTwoPage: (item: SearchDocument) => {
|
||||
setSourceData(item);
|
||||
@@ -142,6 +144,7 @@ function DropdownList({
|
||||
globalItemIndexMap,
|
||||
handleItemAction,
|
||||
isChatMode,
|
||||
formatUrl,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,8 @@ const SearchResultsPanel = memo<{
|
||||
isChatMode: boolean;
|
||||
changeInput: (val: string) => void;
|
||||
changeMode?: (isChatMode: boolean) => void;
|
||||
}>(({ input, isChatMode, changeInput, changeMode }) => {
|
||||
formatUrl?: (item: string) => string;
|
||||
}>(({ input, isChatMode, changeInput, changeMode, formatUrl }) => {
|
||||
const {
|
||||
sourceData,
|
||||
goAskAi,
|
||||
@@ -90,6 +91,7 @@ const SearchResultsPanel = memo<{
|
||||
isError={isError}
|
||||
isSearchComplete={isSearchComplete}
|
||||
isChatMode={isChatMode}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -100,6 +102,7 @@ interface SearchProps {
|
||||
input: string;
|
||||
setIsPinned?: (value: boolean) => void;
|
||||
changeMode?: (isChatMode: boolean) => void;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
function Search({
|
||||
@@ -108,6 +111,7 @@ function Search({
|
||||
input,
|
||||
setIsPinned,
|
||||
changeMode,
|
||||
formatUrl,
|
||||
}: SearchProps) {
|
||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -118,11 +122,12 @@ function Search({
|
||||
isChatMode={isChatMode}
|
||||
changeInput={changeInput}
|
||||
changeMode={changeMode}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
|
||||
<Footer setIsPinnedWeb={setIsPinned} />
|
||||
|
||||
<ContextMenu />
|
||||
<ContextMenu formatUrl={formatUrl}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import { useAppStore } from "@/stores/appStore";
|
||||
interface SearchResultsProps {
|
||||
input: string;
|
||||
isChatMode: boolean;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
export function SearchResults({ input, isChatMode }: SearchResultsProps) {
|
||||
export function SearchResults({ input, isChatMode, formatUrl }: SearchResultsProps) {
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState("1");
|
||||
@@ -46,6 +47,7 @@ export function SearchResults({ input, isChatMode }: SearchResultsProps) {
|
||||
isChatMode={isChatMode}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
|
||||
{/* Right Panel */}
|
||||
|
||||
@@ -42,6 +42,7 @@ interface SearchChatProps {
|
||||
isMobile?: boolean;
|
||||
assistantIDs?: string[];
|
||||
startPage?: StartPage;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
function SearchChat({
|
||||
@@ -57,6 +58,7 @@ function SearchChat({
|
||||
isMobile = false,
|
||||
assistantIDs,
|
||||
startPage,
|
||||
formatUrl,
|
||||
}: SearchChatProps) {
|
||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||
|
||||
@@ -331,6 +333,7 @@ function SearchChat({
|
||||
changeInput={setInput}
|
||||
setIsPinned={setIsPinned}
|
||||
changeMode={changeMode}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -254,7 +254,8 @@ export function useChatActions(
|
||||
[chatHistory, sendMessage]
|
||||
);
|
||||
|
||||
const handleChatCreateStreamMessage = useCallback((msg: string) => {
|
||||
const handleChatCreateStreamMessage = useCallback(
|
||||
(msg: string) => {
|
||||
if (
|
||||
msg.includes("_id") &&
|
||||
msg.includes("_source") &&
|
||||
@@ -292,8 +293,19 @@ export function useChatActions(
|
||||
setVisibleStartPage(false);
|
||||
return;
|
||||
}
|
||||
|
||||
dealMsgRef.current?.(msg);
|
||||
}, [curIdRef, updatedChatRef, changeInput, setActiveChat, setCurChatEnd, setVisibleStartPage, dealMsgRef]);
|
||||
},
|
||||
[
|
||||
curIdRef,
|
||||
updatedChatRef,
|
||||
changeInput,
|
||||
setActiveChat,
|
||||
setCurChatEnd,
|
||||
setVisibleStartPage,
|
||||
dealMsgRef,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri || !currentService?.id) return;
|
||||
|
||||
@@ -15,6 +15,7 @@ interface UseKeyboardNavigationProps {
|
||||
globalItemIndexMap: Record<number, SearchDocument>;
|
||||
handleItemAction: (item: SearchDocument) => void;
|
||||
isChatMode: boolean;
|
||||
formatUrl?: (item: any) => string;
|
||||
}
|
||||
|
||||
export function useKeyboardNavigation({
|
||||
@@ -27,6 +28,7 @@ export function useKeyboardNavigation({
|
||||
globalItemIndexMap,
|
||||
handleItemAction,
|
||||
isChatMode,
|
||||
formatUrl,
|
||||
}: UseKeyboardNavigationProps) {
|
||||
const openPopover = useShortcutsStore((state) => state.openPopover);
|
||||
const visibleContextMenu = useSearchStore((state) => {
|
||||
@@ -109,7 +111,7 @@ export function useKeyboardNavigation({
|
||||
if (e.key === "Enter" && !e.shiftKey && selectedIndex !== null) {
|
||||
const item = globalItemIndexMap[selectedIndex];
|
||||
|
||||
return platformAdapter.openSearchItem(item);
|
||||
return platformAdapter.openSearchItem(item, formatUrl);
|
||||
}
|
||||
|
||||
if (e.key >= "0" && e.key <= "9" && showIndex && modifierKeyPressed) {
|
||||
@@ -121,7 +123,7 @@ export function useKeyboardNavigation({
|
||||
|
||||
const item = globalItemIndexMap[index];
|
||||
|
||||
platformAdapter.openSearchItem(item);
|
||||
platformAdapter.openSearchItem(item, formatUrl);
|
||||
}
|
||||
},
|
||||
[suggests, selectedIndex, showIndex, globalItemIndexMap, openPopover]
|
||||
|
||||
@@ -25,6 +25,7 @@ export function useMessageHandler(
|
||||
) {
|
||||
const messageTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const connectionTimeout = useConnectStore((state) => state.connectionTimeout);
|
||||
const inThinkRef = useRef<boolean>(false);
|
||||
|
||||
const dealMsg = useCallback(
|
||||
(msg: string) => {
|
||||
@@ -54,6 +55,8 @@ export function useMessageHandler(
|
||||
[chunkData.chunk_type]: true,
|
||||
}));
|
||||
|
||||
|
||||
|
||||
if (chunkData.chunk_type === "query_intent") {
|
||||
handlers.deal_query_intent(chunkData);
|
||||
} else if (chunkData.chunk_type === "tools") {
|
||||
@@ -67,7 +70,28 @@ export function useMessageHandler(
|
||||
} else if (chunkData.chunk_type === "think") {
|
||||
handlers.deal_think(chunkData);
|
||||
} else if (chunkData.chunk_type === "response") {
|
||||
const message_chunk = chunkData.message_chunk;
|
||||
if (typeof message_chunk === "string") {
|
||||
if (
|
||||
message_chunk.includes("\u003cthink\u003e") ||
|
||||
message_chunk.includes("<think>")
|
||||
) {
|
||||
inThinkRef.current = true;
|
||||
return;
|
||||
} else if (
|
||||
message_chunk.includes("\u003c/think\u003e") ||
|
||||
message_chunk.includes("</think>")
|
||||
) {
|
||||
inThinkRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (inThinkRef.current) {
|
||||
handlers.deal_think({...chunkData, chunk_type: "think"});
|
||||
} else {
|
||||
handlers.deal_response(chunkData);
|
||||
}
|
||||
}
|
||||
} else if (chunkData.chunk_type === "reply_end") {
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
|
||||
@@ -26,9 +26,15 @@ const useScript = (src: string, onError?: () => void) => {
|
||||
|
||||
export default useScript;
|
||||
|
||||
export const useIconfontScript = () => {
|
||||
export const useIconfontScript = (type: "web" | "app", serverUrl?: string) => {
|
||||
if (type === "web") {
|
||||
useScript(`${serverUrl}/assets/fonts/icons/iconfont.js`);
|
||||
useScript(`${serverUrl}/assets/fonts/icons-app/iconfont.js`);
|
||||
} else {
|
||||
// Coco Server Icons
|
||||
useScript("https://at.alicdn.com/t/c/font_4878526_cykw3et0ezd.js");
|
||||
|
||||
// Coco App Icons
|
||||
useScript("https://at.alicdn.com/t/c/font_4934333_zclkkzo4fgo.js");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
# SearchChat Web Component API
|
||||
|
||||
A customizable search and chat interface component for web applications.
|
||||
# @infinilabs/search-chat
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @infini/coco-app
|
||||
npm install @infinilabs/search-chat
|
||||
# or
|
||||
pnpm add @infinilabs/search-chat
|
||||
# or
|
||||
yarn add @infinilabs/search-chat
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
## Quick Start
|
||||
|
||||
```jsx
|
||||
import SearchChat from '@infini/coco-app';
|
||||
```tsx
|
||||
import SearchChat from '@infinilabs/search-chat';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<SearchChat
|
||||
serverUrl="https://your-server.com"
|
||||
headers={{
|
||||
"X-API-TOKEN": "your-token",
|
||||
"APP-INTEGRATION-ID": "your-app-id"
|
||||
}}
|
||||
serverUrl="https://your-api-url"
|
||||
width={800}
|
||||
height={600}
|
||||
hasModules={['search', 'chat']}
|
||||
defaultModule="search"
|
||||
assistantIDs={["a1", "a2"]}
|
||||
theme="auto"
|
||||
searchPlaceholder="Please enter search content"
|
||||
chatPlaceholder="Please enter chat content"
|
||||
showChatHistory={true}
|
||||
// other props...
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,116 +36,29 @@ function App() {
|
||||
|
||||
## Props
|
||||
|
||||
### `width`
|
||||
- **Type**: `number`
|
||||
- **Default**: `680`
|
||||
- **Description**: Maximum width of the component in pixels
|
||||
| Name | Type | Description |
|
||||
|--------------------|--------------------------------|----------------------------------------------------------|
|
||||
| headers | Record<string, unknown> | Optional, custom request headers |
|
||||
| serverUrl | string | Optional, backend service URL |
|
||||
| width | number | Optional, component width (pixels) |
|
||||
| height | number | Optional, component height (pixels) |
|
||||
| hasModules | string[] | Optional, enabled modules, e.g. ['search', 'chat'] |
|
||||
| defaultModule | "search" \| "chat" | Optional, default module |
|
||||
| assistantIDs | string[] | Optional, list of available assistant IDs |
|
||||
| theme | "auto" \| "light" \| "dark" | Optional, theme mode |
|
||||
| searchPlaceholder | string | Optional, search input placeholder |
|
||||
| chatPlaceholder | string | Optional, chat input placeholder |
|
||||
| showChatHistory | boolean | Optional, whether to show chat history |
|
||||
| startPage | StartPage | Optional, initial page config (see project definition) |
|
||||
| setIsPinned | (value: boolean) => void | Optional, callback for pinning the component |
|
||||
| onCancel | () => void | Optional, cancel callback |
|
||||
| formatUrl | (item: any) => string | Optional, function to format URLs |
|
||||
| isOpen | boolean | Optional, whether the component is open |
|
||||
|
||||
### `height`
|
||||
- **Type**: `number`
|
||||
- **Default**: `590`
|
||||
- **Description**: Height of the component in pixels
|
||||
> For the `StartPage` type, please refer to the project definition.
|
||||
|
||||
### `headers`
|
||||
- **Type**: `Record<string, unknown>`
|
||||
- **Default**:
|
||||
```typescript
|
||||
{
|
||||
"X-API-TOKEN": "default-token",
|
||||
"APP-INTEGRATION-ID": "default-id"
|
||||
}
|
||||
```
|
||||
- **Description**: HTTP headers for API requests
|
||||
|
||||
### `serverUrl`
|
||||
- **Type**: `string`
|
||||
- **Default**: `""`
|
||||
- **Description**: Base URL for the server API
|
||||
|
||||
### `hasModules`
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `["search", "chat"]`
|
||||
- **Description**: Available modules to show
|
||||
|
||||
### `defaultModule`
|
||||
- **Type**: `"search" | "chat"`
|
||||
- **Default**: `"search"`
|
||||
- **Description**: Initial active module
|
||||
|
||||
### `assistantIDs`
|
||||
- **Type**: `string[]`
|
||||
- **Default**: `[]`
|
||||
- **Description**: List of assistant IDs to use
|
||||
|
||||
### `theme`
|
||||
- **Type**: `"auto" | "light" | "dark"`
|
||||
- **Default**: `"dark"`
|
||||
- **Description**: UI theme setting
|
||||
|
||||
### `searchPlaceholder`
|
||||
- **Type**: `string`
|
||||
- **Default**: `""`
|
||||
- **Description**: Placeholder text for search input
|
||||
|
||||
### `chatPlaceholder`
|
||||
- **Type**: `string`
|
||||
- **Default**: `""`
|
||||
- **Description**: Placeholder text for chat input
|
||||
|
||||
### `showChatHistory`
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `false`
|
||||
- **Description**: Whether to display chat history panel
|
||||
|
||||
### `startPage`
|
||||
- **Type**: `StartPage`
|
||||
- **Optional**: Yes
|
||||
- **Description**: Initial page configuration
|
||||
|
||||
### `setIsPinned`
|
||||
- **Type**: `(value: boolean) => void`
|
||||
- **Optional**: Yes
|
||||
- **Description**: Callback when pin status changes
|
||||
|
||||
### `onCancel`
|
||||
- **Type**: `() => void`
|
||||
- **Optional**: Yes
|
||||
- **Description**: Callback when close button is clicked (mobile only)
|
||||
|
||||
### `isOpen`
|
||||
- **Type**: `boolean`
|
||||
- **Optional**: Yes
|
||||
- **Description**: Control component visibility
|
||||
|
||||
## Events
|
||||
|
||||
The component emits the following events:
|
||||
|
||||
- `onModeChange`: Triggered when switching between search and chat modes
|
||||
- `onCancel`: Triggered when the close button is clicked (mobile only)
|
||||
|
||||
## Mobile Support
|
||||
|
||||
The component is responsive and includes mobile-specific features:
|
||||
- Automatic height adjustment
|
||||
- Close button in top-right corner
|
||||
- Touch-friendly interface
|
||||
|
||||
## Example
|
||||
|
||||
```jsx
|
||||
<SearchChat
|
||||
width={800}
|
||||
height={600}
|
||||
serverUrl="https://api.example.com"
|
||||
headers={{
|
||||
"X-API-TOKEN": "your-token",
|
||||
"APP-INTEGRATION-ID": "your-app-id"
|
||||
}}
|
||||
theme="dark"
|
||||
showChatHistory={true}
|
||||
hasModules={["search", "chat"]}
|
||||
defaultModule="chat"
|
||||
setIsPinned={(isPinned) => console.log('Pinned:', isPinned)}
|
||||
/>
|
||||
```
|
||||
## Notes
|
||||
- Requires React 18 or above.
|
||||
- The component is bundled as ESM format; `react` and `react-dom` must be provided by the host project.
|
||||
- Supports on-demand module loading and custom themes.
|
||||
- For more advanced usage, please refer to the source code or contact the developer.
|
||||
@@ -28,6 +28,7 @@ interface WebAppProps {
|
||||
startPage?: StartPage;
|
||||
setIsPinned?: (value: boolean) => void;
|
||||
onCancel?: () => void;
|
||||
formatUrl?: (item: any) => string;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ function WebApp({
|
||||
startPage,
|
||||
setIsPinned,
|
||||
onCancel,
|
||||
formatUrl,
|
||||
}: WebAppProps) {
|
||||
const setIsTauri = useAppStore((state) => state.setIsTauri);
|
||||
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||
@@ -73,7 +75,7 @@ function WebApp({
|
||||
useEscape();
|
||||
useModifierKeyPress();
|
||||
useViewportHeight();
|
||||
useIconfontScript();
|
||||
useIconfontScript('web', serverUrl);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -116,6 +118,7 @@ function WebApp({
|
||||
isMobile={isMobile}
|
||||
assistantIDs={assistantIDs}
|
||||
startPage={startPage}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function Layout() {
|
||||
platformAdapter.error(message);
|
||||
});
|
||||
|
||||
useIconfontScript();
|
||||
useIconfontScript('app');
|
||||
|
||||
const setDisabledExtensions = useExtensionsStore((state) => {
|
||||
return state.setDisabledExtensions;
|
||||
|
||||
@@ -118,7 +118,7 @@ export interface SystemOperations {
|
||||
commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
|
||||
isWindows10: () => Promise<boolean>;
|
||||
revealItemInDir: (path: string) => Promise<unknown>;
|
||||
openSearchItem: (data: SearchDocument) => Promise<unknown>;
|
||||
openSearchItem: (data: SearchDocument, formatUrl?: (item: SearchDocument) => string) => Promise<unknown>;
|
||||
searchMCPServers: (serverId: string, queryParams: string[]) => Promise<any[]>;
|
||||
searchDataSources: (serverId: string, queryParams: string[]) => Promise<any[]>;
|
||||
fetchAssistant: (serverId: string, queryParams: string[]) => Promise<any>;
|
||||
|
||||
@@ -200,13 +200,14 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
||||
console.log("revealItemInDir is not supported in web environment", path);
|
||||
},
|
||||
|
||||
async openSearchItem(data) {
|
||||
async openSearchItem(data, formatUrl) {
|
||||
if (data.type === "AI Assistant") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.url) {
|
||||
return OpenURLWithBrowser(data.url);
|
||||
const url = (formatUrl && formatUrl(data)) || data.url;
|
||||
if (url) {
|
||||
return OpenURLWithBrowser(url);
|
||||
}
|
||||
|
||||
if (data?.payload?.result?.value) {
|
||||
@@ -217,16 +218,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
||||
error: console.error,
|
||||
|
||||
async searchMCPServers(_serverId, queryParams) {
|
||||
const urlParams = new URLSearchParams();
|
||||
queryParams.forEach((param) => {
|
||||
const [key, value] = param.split("=");
|
||||
urlParams.append(key, decodeURIComponent(value));
|
||||
});
|
||||
|
||||
const [error, res]: any = await Post(
|
||||
"/mcp_server/_search",
|
||||
{},
|
||||
Object.fromEntries(urlParams)
|
||||
`/mcp_server/_search?${queryParams?.join("&")}`,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -244,16 +238,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
||||
},
|
||||
|
||||
async searchDataSources(_serverId, queryParams) {
|
||||
const urlParams = new URLSearchParams();
|
||||
queryParams.forEach((param) => {
|
||||
const [key, value] = param.split("=");
|
||||
urlParams.append(key, decodeURIComponent(value));
|
||||
});
|
||||
|
||||
const [error, res]: any = await Post(
|
||||
"/datasource/_search",
|
||||
{},
|
||||
Object.fromEntries(urlParams)
|
||||
`/datasource/_search?${queryParams?.join("&")}`,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -271,16 +258,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
||||
},
|
||||
|
||||
async fetchAssistant(_serverId, queryParams) {
|
||||
const urlParams = new URLSearchParams();
|
||||
queryParams.forEach((param) => {
|
||||
const [key, value] = param.split("=");
|
||||
urlParams.append(key, decodeURIComponent(value));
|
||||
});
|
||||
|
||||
const [error, res]: any = await Post(
|
||||
"/assistant/_search",
|
||||
{},
|
||||
Object.fromEntries(urlParams)
|
||||
`/assistant/_search?${queryParams?.join("&")}`,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -72,7 +72,7 @@ export default defineConfig({
|
||||
|
||||
const packageJson = {
|
||||
name: "@infinilabs/search-chat",
|
||||
version: "1.2.28",
|
||||
version: "1.2.35",
|
||||
main: "index.js",
|
||||
module: "index.js",
|
||||
type: "module",
|
||||
|
||||
Reference in New Issue
Block a user