mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: add web login (#967)
* feat: add web login * refactor: update * refactor: update
This commit is contained in:
@@ -112,15 +112,11 @@ export const Post = <T>(
|
||||
}
|
||||
|
||||
axios
|
||||
.post(
|
||||
baseURL + url,
|
||||
data,
|
||||
{
|
||||
params,
|
||||
headers,
|
||||
withCredentials: true,
|
||||
} as any
|
||||
)
|
||||
.post(baseURL + url, data, {
|
||||
params,
|
||||
headers,
|
||||
withCredentials: true,
|
||||
} as any)
|
||||
.then((result) => {
|
||||
resolve([null, result.data as FcResponse<T>]);
|
||||
})
|
||||
|
||||
@@ -10,6 +10,9 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
// import SessionFile from "./SessionFile";
|
||||
import ScrollToBottom from "@/components/Common/ScrollToBottom";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { NoResults } from "../Common/UI/NoResults";
|
||||
|
||||
interface ChatContentProps {
|
||||
activeChat?: Chat;
|
||||
@@ -97,87 +100,100 @@ export const ChatContent = ({
|
||||
setIsAtBottom(isAtBottom);
|
||||
};
|
||||
|
||||
const { isTauri } = useAppStore();
|
||||
const { disabled } = useWebConfigStore();
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-hidden flex flex-col justify-between relative user-select-text">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{(!activeChat || activeChat?.messages?.length === 0) &&
|
||||
!visibleStartPage && <Greetings />}
|
||||
{!isTauri && disabled ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{(!activeChat || activeChat?.messages?.length === 0) &&
|
||||
!visibleStartPage && <Greetings />}
|
||||
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
message={message}
|
||||
isTyping={false}
|
||||
onResend={handleSendMessage}
|
||||
/>
|
||||
))}
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
key={message._id + index}
|
||||
message={message}
|
||||
isTyping={false}
|
||||
onResend={handleSendMessage}
|
||||
/>
|
||||
))}
|
||||
|
||||
{(!curChatEnd ||
|
||||
query_intent ||
|
||||
tools ||
|
||||
fetch_source ||
|
||||
pick_source ||
|
||||
deep_read ||
|
||||
think ||
|
||||
response) &&
|
||||
activeChat?._source?.id ? (
|
||||
<ChatMessage
|
||||
key={"current"}
|
||||
message={{
|
||||
_id: "current",
|
||||
_source: {
|
||||
type: "assistant",
|
||||
assistant_id:
|
||||
allMessages[allMessages.length - 1]?._source?.assistant_id,
|
||||
message: "",
|
||||
question: Question,
|
||||
},
|
||||
}}
|
||||
onResend={handleSendMessage}
|
||||
isTyping={!curChatEnd}
|
||||
query_intent={query_intent}
|
||||
tools={tools}
|
||||
fetch_source={fetch_source}
|
||||
pick_source={pick_source}
|
||||
deep_read={deep_read}
|
||||
think={think}
|
||||
response={response}
|
||||
loadingStep={loadingStep}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
) : null}
|
||||
{(!curChatEnd ||
|
||||
query_intent ||
|
||||
tools ||
|
||||
fetch_source ||
|
||||
pick_source ||
|
||||
deep_read ||
|
||||
think ||
|
||||
response) &&
|
||||
activeChat?._source?.id ? (
|
||||
<ChatMessage
|
||||
key={"current"}
|
||||
message={{
|
||||
_id: "current",
|
||||
_source: {
|
||||
type: "assistant",
|
||||
assistant_id:
|
||||
allMessages[allMessages.length - 1]?._source
|
||||
?.assistant_id,
|
||||
message: "",
|
||||
question: Question,
|
||||
},
|
||||
}}
|
||||
onResend={handleSendMessage}
|
||||
isTyping={!curChatEnd}
|
||||
query_intent={query_intent}
|
||||
tools={tools}
|
||||
fetch_source={fetch_source}
|
||||
pick_source={pick_source}
|
||||
deep_read={deep_read}
|
||||
think={think}
|
||||
response={response}
|
||||
loadingStep={loadingStep}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{timedoutShow ? (
|
||||
<ChatMessage
|
||||
key={"timedout"}
|
||||
message={{
|
||||
_id: "timedout",
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: t("assistant.chat.timedout"),
|
||||
question: Question,
|
||||
},
|
||||
}}
|
||||
onResend={handleSendMessage}
|
||||
isTyping={false}
|
||||
/>
|
||||
) : null}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
{timedoutShow ? (
|
||||
<ChatMessage
|
||||
key={"timedout"}
|
||||
message={{
|
||||
_id: "timedout",
|
||||
_source: {
|
||||
type: "assistant",
|
||||
message: t("assistant.chat.timedout"),
|
||||
question: Question,
|
||||
},
|
||||
}}
|
||||
onResend={handleSendMessage}
|
||||
isTyping={false}
|
||||
/>
|
||||
) : null}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{uploadAttachments.length > 0 && (
|
||||
<div key={currentSessionId} className="max-h-[120px] overflow-auto p-2">
|
||||
<AttachmentList />
|
||||
</div>
|
||||
{uploadAttachments.length > 0 && (
|
||||
<div
|
||||
key={currentSessionId}
|
||||
className="max-h-[120px] overflow-auto p-2"
|
||||
>
|
||||
<AttachmentList />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* {currentSessionId && <SessionFile sessionId={currentSessionId} />} */}
|
||||
|
||||
<ScrollToBottom scrollRef={scrollRef} isAtBottom={isAtBottom} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* {currentSessionId && <SessionFile sessionId={currentSessionId} />} */}
|
||||
|
||||
<ScrollToBottom scrollRef={scrollRef} isAtBottom={isAtBottom} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
|
||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||
import Copyright from "@/components/Common/Copyright";
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
@@ -17,6 +16,7 @@ import { useThemeStore } from "@/stores/themeStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import FontIcon from "../Icons/FontIcon";
|
||||
import TogglePin from "../TogglePin";
|
||||
import WebFooter from "./WebFooter";
|
||||
|
||||
interface FooterProps {
|
||||
setIsPinnedWeb?: (value: boolean) => void;
|
||||
@@ -49,7 +49,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
return updateInfo && !skipVersions.includes(updateInfo.version);
|
||||
}, [updateInfo, skipVersions]);
|
||||
|
||||
const renderLeft = () => {
|
||||
const renderTauriLeft = () => {
|
||||
if (sourceData?.source?.name) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -116,12 +116,17 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className="px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none overflow-hidden"
|
||||
className={clsx(
|
||||
"px-4 z-999 mx-[1px] h-8 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-md rounded-t-none",
|
||||
{
|
||||
"overflow-hidden": isTauri,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isTauri ? (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
{renderLeft()}
|
||||
{renderTauriLeft()}
|
||||
|
||||
<TogglePin
|
||||
className={clsx({
|
||||
@@ -132,7 +137,7 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Copyright />
|
||||
<WebFooter />
|
||||
)}
|
||||
|
||||
<div className={`flex mobile:hidden items-center gap-3`}>
|
||||
|
||||
@@ -5,6 +5,11 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import clsx from "clsx";
|
||||
import { formatKey } from "@/utils/keyboardUtils";
|
||||
import SearchEmpty from "../SearchEmpty";
|
||||
import FontIcon from "../Icons/FontIcon";
|
||||
import WebLoginButton from "./WebLoginButton";
|
||||
import WebRefreshButton from "./WebRefreshButton";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
export const NoResults = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -12,33 +17,66 @@ export const NoResults = () => {
|
||||
const modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
|
||||
|
||||
const { isTauri } = useAppStore();
|
||||
const { disabled } = useWebConfigStore();
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isTauri && disabled) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 text-sm">
|
||||
<FontIcon
|
||||
name="font_coco-logo-line"
|
||||
className="size-20 text-[#999]"
|
||||
/>
|
||||
|
||||
<div className="text-center">
|
||||
<p>{t("webLogin.hints.welcome")}</p>
|
||||
<p>{t("webLogin.hints.pleaseLogin")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<WebLoginButton />
|
||||
|
||||
<WebRefreshButton className="size-8" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchEmpty />
|
||||
|
||||
<div
|
||||
className={`flex mobile:hidden mt-10 text-sm text-[#333] dark:text-[#D8D8D8]`}
|
||||
>
|
||||
{t("search.main.askCoco")}
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
"ml-3 h-5 min-w-5 rounded-md border border-[#D8D8D8] flex justify-center items-center",
|
||||
{
|
||||
"px-1": !isMac,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{formatKey(modifierKey)}
|
||||
</span>
|
||||
|
||||
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
{modeSwitch}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full flex flex-col justify-center items-center"
|
||||
>
|
||||
<SearchEmpty />
|
||||
|
||||
<div
|
||||
className={`flex mobile:hidden mt-10 text-sm text-[#333] dark:text-[#D8D8D8]`}
|
||||
>
|
||||
{t("search.main.askCoco")}
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
"ml-3 h-5 min-w-5 rounded-md border border-[#D8D8D8] flex justify-center items-center",
|
||||
{
|
||||
"px-1": !isMac,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{formatKey(modifierKey)}
|
||||
</span>
|
||||
|
||||
<span className="ml-1 w-5 h-5 rounded-[6px] border border-[#D8D8D8] flex justify-center items-center">
|
||||
{modeSwitch}
|
||||
</span>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
98
src/components/Common/UI/WebFooter.tsx
Normal file
98
src/components/Common/UI/WebFooter.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import FontIcon from "../Icons/FontIcon";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { LogOut } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import Copyright from "../Copyright";
|
||||
import WebLoginButton from "./WebLoginButton";
|
||||
import WebRefreshButton from "./WebRefreshButton";
|
||||
import WebUserAvatar from "./WebUserAvatar";
|
||||
import { Post } from "@/api/axiosRequest";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const WebFooter = () => {
|
||||
const { integration, loginInfo, setIntegration, setLoginInfo } =
|
||||
useWebConfigStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Popover>
|
||||
<PopoverButton
|
||||
onMouseDown={() => {
|
||||
console.log("WebFooter PopoverButton click");
|
||||
}}
|
||||
>
|
||||
{loginInfo ? (
|
||||
<WebUserAvatar />
|
||||
) : (
|
||||
<FontIcon
|
||||
name="font_coco-logo-line"
|
||||
className="size-5 text-[#999]"
|
||||
/>
|
||||
)}
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel className="absolute z-50 bottom-5 left-0 w-[300px] rounded-xl bg-white dark:bg-[#202126] text-sm/6 text-[#333] dark:text-[#D8D8D8] shadow-lg border dark:border-white/10 -translate-y-2">
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span>{t("webLogin.title")}</span>
|
||||
|
||||
<WebRefreshButton />
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
{loginInfo ? (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<WebUserAvatar
|
||||
className="!size-12"
|
||||
icon={{ className: "!size-6" }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span>{loginInfo.name}</span>
|
||||
<span className="text-[#999]">{loginInfo.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10"
|
||||
onClick={async () => {
|
||||
await Post("/account/logout", void 0);
|
||||
|
||||
setIntegration(void 0);
|
||||
setLoginInfo(void 0);
|
||||
}}
|
||||
>
|
||||
<LogOut
|
||||
className={clsx(
|
||||
"size-3 text-[#0287FF] transition-transform duration-1000"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<span className="text-[#999]">
|
||||
{integration?.guest?.enabled
|
||||
? t("webLogin.hints.tourist")
|
||||
: t("webLogin.hints.login")}
|
||||
</span>
|
||||
|
||||
<WebLoginButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t dark:border-t-white/10">
|
||||
<Copyright />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebFooter;
|
||||
26
src/components/Common/UI/WebLoginButton.tsx
Normal file
26
src/components/Common/UI/WebLoginButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const WebLoginButton = () => {
|
||||
const { endpoint } = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = () => {
|
||||
window.open(endpoint);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="px-6 h-8 text-white bg-[#0287FF] flex rounded-[8px] items-center justify-center gap-1"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span>{t("webLogin.buttons.login")}</span>
|
||||
|
||||
<SquareArrowOutUpRight className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebLoginButton;
|
||||
48
src/components/Common/UI/WebRefreshButton.tsx
Normal file
48
src/components/Common/UI/WebRefreshButton.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { FC, useState } from "react";
|
||||
import { Button, ButtonProps } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import VisibleKey from "../VisibleKey";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
|
||||
const WebRefreshButton: FC<ButtonProps> = (props) => {
|
||||
const { className, ...rest } = props;
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const { onRefresh } = useWebConfigStore();
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
onClick={handleRefresh}
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-6 bg-white dark:bg-[#202126] rounded-[8px] border dark:border-white/10",
|
||||
className
|
||||
)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={clsx(
|
||||
"size-3 text-[#0287FF] transition-transform duration-1000",
|
||||
{
|
||||
"animate-spin": isRefreshing,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebRefreshButton;
|
||||
24
src/components/Common/UI/WebUserAvatar.tsx
Normal file
24
src/components/Common/UI/WebUserAvatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import clsx from "clsx";
|
||||
import { LucideProps, User } from "lucide-react";
|
||||
import { FC, HTMLAttributes } from "react";
|
||||
|
||||
interface WebUserAvatarProps extends HTMLAttributes<HTMLDivElement> {
|
||||
icon?: LucideProps;
|
||||
}
|
||||
|
||||
const WebUserAvatar: FC<WebUserAvatarProps> = (props) => {
|
||||
const { className, icon } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-center size-5 rounded-full border dark:border-white/10 overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<User {...icon} className={clsx("size-4", icon?.className)}></User>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebUserAvatar;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { useBoolean } from "ahooks";
|
||||
import {
|
||||
useImperativeHandle,
|
||||
@@ -101,6 +103,9 @@ const AutoResizeTextarea = forwardRef<
|
||||
[setInput]
|
||||
);
|
||||
|
||||
const { isTauri } = useAppStore();
|
||||
const { disabled } = useWebConfigStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea
|
||||
@@ -121,6 +126,7 @@ const AutoResizeTextarea = forwardRef<
|
||||
setTimeout(setFalse, 0);
|
||||
}}
|
||||
rows={1}
|
||||
disabled={!isTauri && disabled}
|
||||
/>
|
||||
|
||||
<div ref={calcRef} className="absolute whitespace-nowrap -z-10">
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
|
||||
import SearchPopover from "./SearchPopover";
|
||||
import MCPPopover from "./MCPPopover";
|
||||
import ChatSwitch from "@/components/Common/ChatSwitch";
|
||||
import Copyright from "@/components/Common/Copyright";
|
||||
import type { DataSource } from "@/types/commands";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
@@ -17,6 +16,7 @@ import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { parseSearchQuery, SearchQuery } from "@/utils";
|
||||
import InputUpload from "./InputUpload";
|
||||
import WebFooter from "../Common/UI/WebFooter";
|
||||
|
||||
interface InputControlsProps {
|
||||
isChatMode: boolean;
|
||||
@@ -237,7 +237,7 @@ const InputControls = ({
|
||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) && (
|
||||
<div className="px-[9px]">
|
||||
<Copyright />
|
||||
<WebFooter />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -596,5 +596,17 @@
|
||||
},
|
||||
"deepLink": {
|
||||
"extensionInstallSuccessfully": "Extension installed successfully."
|
||||
},
|
||||
"webLogin": {
|
||||
"title": "Account Information",
|
||||
"hints": {
|
||||
"tourist": "Tourist mode, login to unlock the full experience.",
|
||||
"login": "Please log in to your account to start.",
|
||||
"welcome": "Welcome to Coco AI.",
|
||||
"pleaseLogin": "Please log in to your account to start."
|
||||
},
|
||||
"buttons": {
|
||||
"login": "Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,5 +595,17 @@
|
||||
},
|
||||
"deepLink": {
|
||||
"extensionInstallSuccessfully": "扩展安装成功。"
|
||||
},
|
||||
"webLogin": {
|
||||
"title": "账户信息",
|
||||
"hints": {
|
||||
"tourist": "游客模式,登录解锁完整体验。",
|
||||
"login": "请登录您的账户以开始。",
|
||||
"welcome": "欢迎访问 Coco AI。",
|
||||
"pleaseLogin": "请登录您的帐户开始使用。"
|
||||
},
|
||||
"buttons": {
|
||||
"login": "登录"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import ErrorNotification from "@/components/Common/ErrorNotification";
|
||||
|
||||
import "@/i18n";
|
||||
import "@/web.css";
|
||||
import { Get } from "@/api/axiosRequest";
|
||||
import { useWebConfigStore } from "@/stores/webConfigStore";
|
||||
import { isPlainObject } from "lodash-es";
|
||||
|
||||
interface WebAppProps {
|
||||
headers?: Record<string, unknown>;
|
||||
@@ -32,6 +35,8 @@ interface WebAppProps {
|
||||
formatUrl?: (item: any) => string;
|
||||
isOpen?: boolean;
|
||||
language?: string;
|
||||
settings?: any;
|
||||
refreshSettings?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function WebApp({
|
||||
@@ -45,7 +50,7 @@ function WebApp({
|
||||
hasModules = ["search", "chat"],
|
||||
defaultModule = "search",
|
||||
assistantIDs = [],
|
||||
theme = "dark",
|
||||
theme = "auto",
|
||||
searchPlaceholder = "",
|
||||
chatPlaceholder = "",
|
||||
showChatHistory = false,
|
||||
@@ -53,9 +58,11 @@ function WebApp({
|
||||
setIsPinned,
|
||||
onCancel,
|
||||
formatUrl,
|
||||
language = 'en',
|
||||
language = "en",
|
||||
settings,
|
||||
refreshSettings,
|
||||
}: WebAppProps) {
|
||||
const {setIsTauri, setEndpoint} = useAppStore();
|
||||
const { setIsTauri, setEndpoint } = useAppStore();
|
||||
const setModeSwitch = useShortcutsStore((state) => state.setModeSwitch);
|
||||
const setInternetSearch = useShortcutsStore((state) => {
|
||||
return state.setInternetSearch;
|
||||
@@ -66,11 +73,35 @@ function WebApp({
|
||||
i18n.changeLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
const {
|
||||
integration,
|
||||
loginInfo,
|
||||
setIntegration,
|
||||
setLoginInfo,
|
||||
setOnRefresh,
|
||||
setDisabled,
|
||||
} = useWebConfigStore();
|
||||
|
||||
const getUserProfile = async () => {
|
||||
const [_, result] = await Get("/account/profile");
|
||||
|
||||
if (isPlainObject(result)) {
|
||||
setLoginInfo(result as any);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUserProfile();
|
||||
|
||||
setIsTauri(false);
|
||||
setEndpoint(serverUrl);
|
||||
setModeSwitch("S");
|
||||
setInternetSearch("E");
|
||||
setIntegration(settings);
|
||||
setOnRefresh(async () => {
|
||||
await getUserProfile();
|
||||
return refreshSettings?.();
|
||||
});
|
||||
|
||||
localStorage.setItem("headers", JSON.stringify(headers || {}));
|
||||
}, []);
|
||||
@@ -83,6 +114,10 @@ function WebApp({
|
||||
useModifierKeyPress();
|
||||
useViewportHeight();
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(!loginInfo && !integration?.guest?.enabled);
|
||||
}, [integration, loginInfo]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="searchChat-container"
|
||||
@@ -126,7 +161,7 @@ function WebApp({
|
||||
startPage={startPage}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
<ErrorNotification isTauri={false}/>
|
||||
<ErrorNotification isTauri={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useMount } from "ahooks";
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import LayoutOutlet from "./outlet";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const Layout = () => {
|
||||
const { language } = useAppStore();
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useMount(async () => {
|
||||
await invoke("backend_setup", {
|
||||
await platformAdapter.invokeBackend("backend_setup", {
|
||||
appLang: language,
|
||||
});
|
||||
|
||||
|
||||
51
src/stores/webConfigStore.ts
Normal file
51
src/stores/webConfigStore.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface Integration {
|
||||
guest?: {
|
||||
enabled?: boolean;
|
||||
run_as?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export type IWebAccessControlStore = {
|
||||
integration?: Integration;
|
||||
setIntegration: (integration?: Integration) => void;
|
||||
loginInfo?: LoginInfo;
|
||||
setLoginInfo: (loginInfo?: LoginInfo) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
setOnRefresh: (onRefresh: () => Promise<void>) => void;
|
||||
disabled: boolean;
|
||||
setDisabled: (disabled: boolean) => void;
|
||||
};
|
||||
|
||||
export const useWebConfigStore = create<IWebAccessControlStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
setIntegration: (integration) => {
|
||||
return set({ integration });
|
||||
},
|
||||
setLoginInfo: (loginInfo) => {
|
||||
return set({ loginInfo });
|
||||
},
|
||||
onRefresh: async () => {},
|
||||
setOnRefresh: (onRefresh) => {
|
||||
return set({ onRefresh });
|
||||
},
|
||||
disabled: true,
|
||||
setDisabled: (disabled) => {
|
||||
return set({ disabled });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "web-config-store",
|
||||
partialize: () => ({}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -10,7 +10,6 @@ import { getCurrentWindowService } from "@/commands/windowService";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import i18next from "i18next";
|
||||
|
||||
// 1
|
||||
export async function copyToClipboard(text: string) {
|
||||
const addError = useAppStore.getState().addError;
|
||||
const language = useAppStore.getState().language;
|
||||
|
||||
Reference in New Issue
Block a user