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:
BiggerRain
2025-07-17 09:22:23 +08:00
committed by GitHub
parent 0b5e31a476
commit e3a3849fa4
23 changed files with 193 additions and 348 deletions

View File

@@ -32,6 +32,7 @@
"localstorage", "localstorage",
"lucide", "lucide",
"maximizable", "maximizable",
"mdast",
"meval", "meval",
"Minimizable", "Minimizable",
"msvc", "msvc",

View File

@@ -37,6 +37,7 @@ Information about release notes of Coco Server is provided here.
- chore: rename QuickLink/quick_link to Quicklink/quicklink #752 - chore: rename QuickLink/quick_link to Quicklink/quicklink #752
- chore: assistant params & styles #753 - chore: assistant params & styles #753
- chore: make optional fields optional #758 - chore: make optional fields optional #758
- chore: search-chat components add formatUrl & think data & icons url #765
## 0.6.0 (2025-06-29) ## 0.6.0 (2025-06-29)

View File

@@ -96,7 +96,7 @@ export const Get = <T>(
export const Post = <T>( export const Post = <T>(
url: string, url: string,
data: IAnyObj, data: IAnyObj | undefined,
params: IAnyObj = {}, params: IAnyObj = {},
headers: IAnyObj = {} headers: IAnyObj = {}
): Promise<[any, FcResponse<T> | undefined]> => { ): Promise<[any, FcResponse<T> | undefined]> => {

View File

@@ -15,11 +15,14 @@ export async function streamPost({
}) { }) {
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}"); const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
let baseURL = appStore.state?.endpoint_http let baseURL = appStore.state?.endpoint_http;
if (!baseURL || baseURL === "undefined") { if (!baseURL || baseURL === "undefined") {
baseURL = ""; baseURL = "";
} }
const headersStr = localStorage.getItem("headers") || "{}";
const headersStorage = JSON.parse(headersStr);
const query = new URLSearchParams(queryParams || {}).toString(); const query = new URLSearchParams(queryParams || {}).toString();
const fullUrl = `${baseURL}${url}?${query}`; const fullUrl = `${baseURL}${url}?${query}`;
@@ -28,6 +31,7 @@ export async function streamPost({
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(headersStorage),
...(headers || {}), ...(headers || {}),
}, },
body: JSON.stringify(body), body: JSON.stringify(body),

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,11 @@ interface State {
activeMenuIndex: number; activeMenuIndex: number;
} }
const ContextMenu = () => { interface ContextMenuProps {
formatUrl?: (item: any) => string;
}
const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const state = useReactive<State>({ const state = useReactive<State>({
@@ -122,7 +126,10 @@ const ContextMenu = () => {
shortcut: "enter", shortcut: "enter",
hide: category === "Calculator", hide: category === "Calculator",
clickEvent: () => { clickEvent: () => {
platformAdapter.openSearchItem(selectedSearchContent as any); platformAdapter.openSearchItem(
selectedSearchContent as any,
formatUrl
);
}, },
}, },
{ {
@@ -135,7 +142,7 @@ const ContextMenu = () => {
type === "AI Assistant" || type === "AI Assistant" ||
id === "Extension Store", id === "Extension Store",
clickEvent() { clickEvent() {
copyToClipboard(url); copyToClipboard(formatUrl && formatUrl(selectedSearchContent) || url);
}, },
}, },
{ {

View File

@@ -20,6 +20,7 @@ interface DocumentListProps {
selectedId?: string; selectedId?: string;
viewMode: "detail" | "list"; viewMode: "detail" | "list";
setViewMode: (mode: "detail" | "list") => void; setViewMode: (mode: "detail" | "list") => void;
formatUrl?: (item: any) => string;
} }
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
@@ -30,6 +31,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
isChatMode, isChatMode,
viewMode, viewMode,
setViewMode, setViewMode,
formatUrl,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sourceData = useSearchStore((state) => state.sourceData); const sourceData = useSearchStore((state) => state.sourceData);
@@ -174,7 +176,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
if (selectedItem === null) return; if (selectedItem === null) return;
const item = data.list[selectedItem]?.document; const item = data.list[selectedItem]?.document;
platformAdapter.openSearchItem(item); platformAdapter.openSearchItem(item, formatUrl);
}; };
switch (e.key) { switch (e.key) {
@@ -238,7 +240,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
currentIndex={index} currentIndex={index}
onMouseEnter={() => onMouseEnter(index, hit.document)} onMouseEnter={() => onMouseEnter(index, hit.document)}
onItemClick={() => { onItemClick={() => {
platformAdapter.openSearchItem(hit.document); platformAdapter.openSearchItem(hit.document, formatUrl);
}} }}
showListRight={viewMode === "list"} showListRight={viewMode === "list"}
/> />

View File

@@ -26,6 +26,7 @@ interface DropdownListProps {
isSearchComplete: boolean; isSearchComplete: boolean;
isChatMode: boolean; isChatMode: boolean;
globalItemIndexMap: Record<number, SearchDocument>; globalItemIndexMap: Record<number, SearchDocument>;
formatUrl?: (item: any) => string;
} }
function DropdownList({ function DropdownList({
@@ -34,6 +35,7 @@ function DropdownList({
isError, isError,
isChatMode, isChatMode,
globalItemIndexMap, globalItemIndexMap,
formatUrl,
}: DropdownListProps) { }: DropdownListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -77,7 +79,7 @@ function DropdownList({
setSelectedSearchContent(item); setSelectedSearchContent(item);
}, },
onItemClick: (item: SearchDocument) => { onItemClick: (item: SearchDocument) => {
platformAdapter.openSearchItem(item); platformAdapter.openSearchItem(item, formatUrl);
}, },
goToTwoPage: (item: SearchDocument) => { goToTwoPage: (item: SearchDocument) => {
setSourceData(item); setSourceData(item);
@@ -142,6 +144,7 @@ function DropdownList({
globalItemIndexMap, globalItemIndexMap,
handleItemAction, handleItemAction,
isChatMode, isChatMode,
formatUrl,
}); });
return ( return (

View File

@@ -16,7 +16,8 @@ const SearchResultsPanel = memo<{
isChatMode: boolean; isChatMode: boolean;
changeInput: (val: string) => void; changeInput: (val: string) => void;
changeMode?: (isChatMode: boolean) => void; changeMode?: (isChatMode: boolean) => void;
}>(({ input, isChatMode, changeInput, changeMode }) => { formatUrl?: (item: string) => string;
}>(({ input, isChatMode, changeInput, changeMode, formatUrl }) => {
const { const {
sourceData, sourceData,
goAskAi, goAskAi,
@@ -90,6 +91,7 @@ const SearchResultsPanel = memo<{
isError={isError} isError={isError}
isSearchComplete={isSearchComplete} isSearchComplete={isSearchComplete}
isChatMode={isChatMode} isChatMode={isChatMode}
formatUrl={formatUrl}
/> />
); );
}); });
@@ -100,6 +102,7 @@ interface SearchProps {
input: string; input: string;
setIsPinned?: (value: boolean) => void; setIsPinned?: (value: boolean) => void;
changeMode?: (isChatMode: boolean) => void; changeMode?: (isChatMode: boolean) => void;
formatUrl?: (item: any) => string;
} }
function Search({ function Search({
@@ -108,6 +111,7 @@ function Search({
input, input,
setIsPinned, setIsPinned,
changeMode, changeMode,
formatUrl,
}: SearchProps) { }: SearchProps) {
const mainWindowRef = useRef<HTMLDivElement>(null); const mainWindowRef = useRef<HTMLDivElement>(null);
@@ -118,11 +122,12 @@ function Search({
isChatMode={isChatMode} isChatMode={isChatMode}
changeInput={changeInput} changeInput={changeInput}
changeMode={changeMode} changeMode={changeMode}
formatUrl={formatUrl}
/> />
<Footer setIsPinnedWeb={setIsPinned} /> <Footer setIsPinnedWeb={setIsPinned} />
<ContextMenu /> <ContextMenu formatUrl={formatUrl}/>
</div> </div>
); );
} }

View File

@@ -7,9 +7,10 @@ import { useAppStore } from "@/stores/appStore";
interface SearchResultsProps { interface SearchResultsProps {
input: string; input: string;
isChatMode: boolean; 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 isTauri = useAppStore((state) => state.isTauri);
const [selectedDocumentId, setSelectedDocumentId] = useState("1"); const [selectedDocumentId, setSelectedDocumentId] = useState("1");
@@ -46,6 +47,7 @@ export function SearchResults({ input, isChatMode }: SearchResultsProps) {
isChatMode={isChatMode} isChatMode={isChatMode}
viewMode={viewMode} viewMode={viewMode}
setViewMode={setViewMode} setViewMode={setViewMode}
formatUrl={formatUrl}
/> />
{/* Right Panel */} {/* Right Panel */}

View File

@@ -42,6 +42,7 @@ interface SearchChatProps {
isMobile?: boolean; isMobile?: boolean;
assistantIDs?: string[]; assistantIDs?: string[];
startPage?: StartPage; startPage?: StartPage;
formatUrl?: (item: any) => string;
} }
function SearchChat({ function SearchChat({
@@ -57,6 +58,7 @@ function SearchChat({
isMobile = false, isMobile = false,
assistantIDs, assistantIDs,
startPage, startPage,
formatUrl,
}: SearchChatProps) { }: SearchChatProps) {
const currentAssistant = useConnectStore((state) => state.currentAssistant); const currentAssistant = useConnectStore((state) => state.currentAssistant);
@@ -331,6 +333,7 @@ function SearchChat({
changeInput={setInput} changeInput={setInput}
setIsPinned={setIsPinned} setIsPinned={setIsPinned}
changeMode={changeMode} changeMode={changeMode}
formatUrl={formatUrl}
/> />
</Suspense> </Suspense>
</div> </div>

View File

@@ -162,7 +162,7 @@ export function useChatActions(
url: "/chat/_create", url: "/chat/_create",
body: { message: value }, body: { message: value },
queryParams, queryParams,
onMessage: (line) => { onMessage: (line) => {
console.log("⏳", line); console.log("⏳", line);
handleChatCreateStreamMessage(line); handleChatCreateStreamMessage(line);
// append to chat box // append to chat box
@@ -254,46 +254,58 @@ export function useChatActions(
[chatHistory, sendMessage] [chatHistory, sendMessage]
); );
const handleChatCreateStreamMessage = useCallback((msg: string) => { const handleChatCreateStreamMessage = useCallback(
if ( (msg: string) => {
msg.includes("_id") && if (
msg.includes("_source") && msg.includes("_id") &&
msg.includes("result") msg.includes("_source") &&
) { msg.includes("result")
const response = JSON.parse(msg); ) {
console.log("first", response); const response = JSON.parse(msg);
let updatedChat: Chat; console.log("first", response);
if (Array.isArray(response)) { let updatedChat: Chat;
curIdRef.current = response[0]?._id; if (Array.isArray(response)) {
updatedChat = { curIdRef.current = response[0]?._id;
...updatedChatRef.current, updatedChat = {
messages: [ ...updatedChatRef.current,
...(updatedChatRef.current?.messages || []), messages: [
...(response || []), ...(updatedChatRef.current?.messages || []),
], ...(response || []),
}; ],
console.log("array", updatedChat, updatedChatRef.current?.messages); };
} else { console.log("array", updatedChat, updatedChatRef.current?.messages);
const newChat: Chat = response; } else {
curIdRef.current = response?.payload?.id; const newChat: Chat = response;
curIdRef.current = response?.payload?.id;
newChat._source = { newChat._source = {
...response?.payload, ...response?.payload,
}; };
updatedChat = { updatedChat = {
...newChat, ...newChat,
messages: [newChat], messages: [newChat],
}; };
}
changeInput && changeInput("");
setActiveChat(updatedChat);
setCurChatEnd(false);
setVisibleStartPage(false);
return;
} }
changeInput && changeInput(""); dealMsgRef.current?.(msg);
setActiveChat(updatedChat); },
setCurChatEnd(false); [
setVisibleStartPage(false); curIdRef,
return; updatedChatRef,
} changeInput,
dealMsgRef.current?.(msg); setActiveChat,
}, [curIdRef, updatedChatRef, changeInput, setActiveChat, setCurChatEnd, setVisibleStartPage, dealMsgRef]); setCurChatEnd,
setVisibleStartPage,
dealMsgRef,
]
);
useEffect(() => { useEffect(() => {
if (!isTauri || !currentService?.id) return; if (!isTauri || !currentService?.id) return;

View File

@@ -15,6 +15,7 @@ interface UseKeyboardNavigationProps {
globalItemIndexMap: Record<number, SearchDocument>; globalItemIndexMap: Record<number, SearchDocument>;
handleItemAction: (item: SearchDocument) => void; handleItemAction: (item: SearchDocument) => void;
isChatMode: boolean; isChatMode: boolean;
formatUrl?: (item: any) => string;
} }
export function useKeyboardNavigation({ export function useKeyboardNavigation({
@@ -27,6 +28,7 @@ export function useKeyboardNavigation({
globalItemIndexMap, globalItemIndexMap,
handleItemAction, handleItemAction,
isChatMode, isChatMode,
formatUrl,
}: UseKeyboardNavigationProps) { }: UseKeyboardNavigationProps) {
const openPopover = useShortcutsStore((state) => state.openPopover); const openPopover = useShortcutsStore((state) => state.openPopover);
const visibleContextMenu = useSearchStore((state) => { const visibleContextMenu = useSearchStore((state) => {
@@ -109,7 +111,7 @@ export function useKeyboardNavigation({
if (e.key === "Enter" && !e.shiftKey && selectedIndex !== null) { if (e.key === "Enter" && !e.shiftKey && selectedIndex !== null) {
const item = globalItemIndexMap[selectedIndex]; const item = globalItemIndexMap[selectedIndex];
return platformAdapter.openSearchItem(item); return platformAdapter.openSearchItem(item, formatUrl);
} }
if (e.key >= "0" && e.key <= "9" && showIndex && modifierKeyPressed) { if (e.key >= "0" && e.key <= "9" && showIndex && modifierKeyPressed) {
@@ -121,7 +123,7 @@ export function useKeyboardNavigation({
const item = globalItemIndexMap[index]; const item = globalItemIndexMap[index];
platformAdapter.openSearchItem(item); platformAdapter.openSearchItem(item, formatUrl);
} }
}, },
[suggests, selectedIndex, showIndex, globalItemIndexMap, openPopover] [suggests, selectedIndex, showIndex, globalItemIndexMap, openPopover]

View File

@@ -25,6 +25,7 @@ export function useMessageHandler(
) { ) {
const messageTimeoutRef = useRef<NodeJS.Timeout>(); const messageTimeoutRef = useRef<NodeJS.Timeout>();
const connectionTimeout = useConnectStore((state) => state.connectionTimeout); const connectionTimeout = useConnectStore((state) => state.connectionTimeout);
const inThinkRef = useRef<boolean>(false);
const dealMsg = useCallback( const dealMsg = useCallback(
(msg: string) => { (msg: string) => {
@@ -54,6 +55,8 @@ export function useMessageHandler(
[chunkData.chunk_type]: true, [chunkData.chunk_type]: true,
})); }));
if (chunkData.chunk_type === "query_intent") { if (chunkData.chunk_type === "query_intent") {
handlers.deal_query_intent(chunkData); handlers.deal_query_intent(chunkData);
} else if (chunkData.chunk_type === "tools") { } else if (chunkData.chunk_type === "tools") {
@@ -67,7 +70,28 @@ export function useMessageHandler(
} else if (chunkData.chunk_type === "think") { } else if (chunkData.chunk_type === "think") {
handlers.deal_think(chunkData); handlers.deal_think(chunkData);
} else if (chunkData.chunk_type === "response") { } else if (chunkData.chunk_type === "response") {
handlers.deal_response(chunkData); 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") { } else if (chunkData.chunk_type === "reply_end") {
if (messageTimeoutRef.current) { if (messageTimeoutRef.current) {
clearTimeout(messageTimeoutRef.current); clearTimeout(messageTimeoutRef.current);

View File

@@ -26,9 +26,15 @@ const useScript = (src: string, onError?: () => void) => {
export default useScript; export default useScript;
export const useIconfontScript = () => { export const useIconfontScript = (type: "web" | "app", serverUrl?: string) => {
// Coco Server Icons if (type === "web") {
useScript("https://at.alicdn.com/t/c/font_4878526_cykw3et0ezd.js"); useScript(`${serverUrl}/assets/fonts/icons/iconfont.js`);
// Coco App Icons useScript(`${serverUrl}/assets/fonts/icons-app/iconfont.js`);
useScript("https://at.alicdn.com/t/c/font_4934333_zclkkzo4fgo.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");
}
}; };

View File

@@ -1,26 +1,34 @@
# SearchChat Web Component API # @infinilabs/search-chat
A customizable search and chat interface component for web applications.
## Installation ## Installation
```bash ```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 ```tsx
import SearchChat from '@infini/coco-app'; import SearchChat from '@infinilabs/search-chat';
function App() { function App() {
return ( return (
<SearchChat <SearchChat
serverUrl="https://your-server.com" serverUrl="https://your-api-url"
headers={{ width={800}
"X-API-TOKEN": "your-token", height={600}
"APP-INTEGRATION-ID": "your-app-id" 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 ## Props
### `width` | Name | Type | Description |
- **Type**: `number` |--------------------|--------------------------------|----------------------------------------------------------|
- **Default**: `680` | headers | Record<string, unknown> | Optional, custom request headers |
- **Description**: Maximum width of the component in pixels | 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` > For the `StartPage` type, please refer to the project definition.
- **Type**: `number`
- **Default**: `590`
- **Description**: Height of the component in pixels
### `headers` ## Notes
- **Type**: `Record<string, unknown>` - Requires React 18 or above.
- **Default**: - The component is bundled as ESM format; `react` and `react-dom` must be provided by the host project.
```typescript - Supports on-demand module loading and custom themes.
{ - For more advanced usage, please refer to the source code or contact the developer.
"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)}
/>
```

View File

@@ -28,6 +28,7 @@ interface WebAppProps {
startPage?: StartPage; startPage?: StartPage;
setIsPinned?: (value: boolean) => void; setIsPinned?: (value: boolean) => void;
onCancel?: () => void; onCancel?: () => void;
formatUrl?: (item: any) => string;
isOpen?: boolean; isOpen?: boolean;
} }
@@ -49,6 +50,7 @@ function WebApp({
startPage, startPage,
setIsPinned, setIsPinned,
onCancel, onCancel,
formatUrl,
}: WebAppProps) { }: WebAppProps) {
const setIsTauri = useAppStore((state) => state.setIsTauri); const setIsTauri = useAppStore((state) => state.setIsTauri);
const setEndpoint = useAppStore((state) => state.setEndpoint); const setEndpoint = useAppStore((state) => state.setEndpoint);
@@ -73,7 +75,7 @@ function WebApp({
useEscape(); useEscape();
useModifierKeyPress(); useModifierKeyPress();
useViewportHeight(); useViewportHeight();
useIconfontScript(); useIconfontScript('web', serverUrl);
return ( return (
<div <div
@@ -116,6 +118,7 @@ function WebApp({
isMobile={isMobile} isMobile={isMobile}
assistantIDs={assistantIDs} assistantIDs={assistantIDs}
startPage={startPage} startPage={startPage}
formatUrl={formatUrl}
/> />
</div> </div>
); );

View File

@@ -108,7 +108,7 @@ export default function Layout() {
platformAdapter.error(message); platformAdapter.error(message);
}); });
useIconfontScript(); useIconfontScript('app');
const setDisabledExtensions = useExtensionsStore((state) => { const setDisabledExtensions = useExtensionsStore((state) => {
return state.setDisabledExtensions; return state.setDisabledExtensions;

View File

@@ -118,7 +118,7 @@ export interface SystemOperations {
commands: <T>(commandName: string, ...args: any[]) => Promise<T>; commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
isWindows10: () => Promise<boolean>; isWindows10: () => Promise<boolean>;
revealItemInDir: (path: string) => Promise<unknown>; 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[]>; searchMCPServers: (serverId: string, queryParams: string[]) => Promise<any[]>;
searchDataSources: (serverId: string, queryParams: string[]) => Promise<any[]>; searchDataSources: (serverId: string, queryParams: string[]) => Promise<any[]>;
fetchAssistant: (serverId: string, queryParams: string[]) => Promise<any>; fetchAssistant: (serverId: string, queryParams: string[]) => Promise<any>;

View File

@@ -200,13 +200,14 @@ export const createWebAdapter = (): WebPlatformAdapter => {
console.log("revealItemInDir is not supported in web environment", path); console.log("revealItemInDir is not supported in web environment", path);
}, },
async openSearchItem(data) { async openSearchItem(data, formatUrl) {
if (data.type === "AI Assistant") { if (data.type === "AI Assistant") {
return; return;
} }
if (data?.url) { const url = (formatUrl && formatUrl(data)) || data.url;
return OpenURLWithBrowser(data.url); if (url) {
return OpenURLWithBrowser(url);
} }
if (data?.payload?.result?.value) { if (data?.payload?.result?.value) {
@@ -217,16 +218,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
error: console.error, error: console.error,
async searchMCPServers(_serverId, queryParams) { 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( const [error, res]: any = await Post(
"/mcp_server/_search", `/mcp_server/_search?${queryParams?.join("&")}`,
{}, undefined
Object.fromEntries(urlParams)
); );
if (error) { if (error) {
@@ -244,16 +238,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
}, },
async searchDataSources(_serverId, queryParams) { 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( const [error, res]: any = await Post(
"/datasource/_search", `/datasource/_search?${queryParams?.join("&")}`,
{}, undefined
Object.fromEntries(urlParams)
); );
if (error) { if (error) {
@@ -271,16 +258,9 @@ export const createWebAdapter = (): WebPlatformAdapter => {
}, },
async fetchAssistant(_serverId, queryParams) { 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( const [error, res]: any = await Post(
"/assistant/_search", `/assistant/_search?${queryParams?.join("&")}`,
{}, undefined
Object.fromEntries(urlParams)
); );
if (error) { if (error) {

View File

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