mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-23 23:09:25 +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",
|
"localstorage",
|
||||||
"lucide",
|
"lucide",
|
||||||
"maximizable",
|
"maximizable",
|
||||||
|
"mdast",
|
||||||
"meval",
|
"meval",
|
||||||
"Minimizable",
|
"Minimizable",
|
||||||
"msvc",
|
"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: 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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]> => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user