mirror of
https://github.com/ONLYOFFICE/desktop-sdk.git
synced 2026-02-10 18:15:05 +08:00
Merge pull request 'bugfix/ai-agent' (#46) from bugfix/ai-agent into release/v9.2.0
Reviewed-on: https://git.onlyoffice.com/ONLYOFFICE/desktop-sdk/pulls/46
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -30,6 +30,7 @@ const DropDownItem = ({
|
||||
withSpace,
|
||||
}: DropDownItemProps) => {
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
const [submenuSide, setSubmenuSide] = useState<"left" | "right">("right");
|
||||
|
||||
const itemRef = useRef<HTMLDivElement | null>(null);
|
||||
const submenuRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -88,6 +89,39 @@ const DropDownItem = ({
|
||||
const handleMouseEnter = () => {
|
||||
if (isSubMenuOpen || !subMenu) return;
|
||||
|
||||
// Calculate which side has more space
|
||||
if (itemRef.current) {
|
||||
const itemRect = itemRef.current.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
const spaceOnRight = viewportWidth - itemRect.right;
|
||||
|
||||
// Assume submenu width is around 300px (max-w-[300px]) + 4px offset
|
||||
const estimatedSubmenuWidth = 304;
|
||||
|
||||
// Open on left if there's not enough space on right but enough on left
|
||||
let side: "left" | "right" = "right";
|
||||
if (spaceOnRight < estimatedSubmenuWidth) {
|
||||
side = "left";
|
||||
}
|
||||
|
||||
setSubmenuSide(side);
|
||||
|
||||
if (side === "left")
|
||||
// Apply positioning after a short delay to ensure the submenu is rendered
|
||||
setTimeout(() => {
|
||||
if (submenuRef.current) {
|
||||
submenuRef.current.style.position = "fixed";
|
||||
|
||||
if (side === "left") {
|
||||
submenuRef.current.style.left = "unset";
|
||||
submenuRef.current.style.bottom = "-19px";
|
||||
submenuRef.current.style.right = `121px`;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
setIsSubMenuOpen(true);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
@ -137,11 +171,11 @@ const DropDownItem = ({
|
||||
/>
|
||||
}
|
||||
items={subMenu}
|
||||
side="right"
|
||||
side={submenuSide}
|
||||
align="start"
|
||||
sideOffset={0}
|
||||
sideOffset={4}
|
||||
open={isSubMenuOpen}
|
||||
contentClassName="ms-[12px] mt-[-15px] max-w-[300px]"
|
||||
contentClassName="mt-[-15px] max-w-[300px]"
|
||||
containerRef={itemRef.current}
|
||||
dropdownRef={submenuRef}
|
||||
/>
|
||||
|
||||
@ -60,15 +60,15 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
const isSettings = currentPage === "settings";
|
||||
|
||||
return (
|
||||
<div className={`h-dvh ${theme}`}>
|
||||
<div className={`h-[100vh] ${theme}`}>
|
||||
<main
|
||||
id="app"
|
||||
className="h-dvh bg-[var(--layout-background-color)] flex flex-col"
|
||||
className="h-[100vh] bg-[var(--layout-background-color)] flex flex-col"
|
||||
>
|
||||
<Navigation />
|
||||
<div
|
||||
className="flex flex-row flex-1"
|
||||
style={{ height: "calc(100dvh - 56px)" }}
|
||||
style={{ height: "calc(100vh - 56px)" }}
|
||||
>
|
||||
{!isSettings ? <ChatList /> : null}
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import useServersStore from "@/store/useServersStore";
|
||||
@ -24,10 +24,10 @@ const ManageToolDialog = ({
|
||||
|
||||
const [isAllowAlways, setIsAllowAlways] = useState(false);
|
||||
|
||||
const onAllowAction = () => {
|
||||
const onAllowAction = React.useCallback(() => {
|
||||
onAllow(isAllowAlways);
|
||||
onClose();
|
||||
};
|
||||
}, [onAllow, isAllowAlways, onClose]);
|
||||
|
||||
const onDenyAction = () => {
|
||||
onDeny();
|
||||
@ -36,6 +36,21 @@ const ManageToolDialog = ({
|
||||
|
||||
const toolCall = manageToolData?.message?.content[manageToolData.idx];
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onAllowAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onAllowAction]);
|
||||
|
||||
if (
|
||||
!toolCall ||
|
||||
typeof toolCall !== "object" ||
|
||||
|
||||
@ -7,3 +7,12 @@
|
||||
:root {
|
||||
font-family: Arial, "Segoe UI", system-ui, Avenir, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background-color: var(--background-normal-element) !important;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--background-scroll-thumb) !important;
|
||||
}
|
||||
@ -39,12 +39,16 @@ export const convertMessagesToMd = (messages: ThreadMessageLike[]) => {
|
||||
return content;
|
||||
};
|
||||
|
||||
export const removeSpecialCharacter = (str: string) => {
|
||||
return str.replace(/[\\/:*"<>|?]/g, "");
|
||||
};
|
||||
|
||||
export const getMessageTitleFromMd = (md: string) => {
|
||||
const lines = md.split("\n");
|
||||
|
||||
const title = lines[0].replace("## ", "");
|
||||
|
||||
return title.substring(0, 30);
|
||||
return removeSpecialCharacter(title).substring(0, 30);
|
||||
};
|
||||
|
||||
export const isDocument = (type: number) => {
|
||||
|
||||
@ -27,7 +27,7 @@ const Composer = () => {
|
||||
|
||||
<ComposerPrimitive.Input
|
||||
placeholder={t("AskAI")}
|
||||
className="composer-input max-h-[calc(50dvh)] min-h-[16px] w-full resize-none outline-none"
|
||||
className="composer-input max-h-[calc(50vh)] min-h-[16px] w-full resize-none outline-none"
|
||||
rows={1}
|
||||
autoFocus
|
||||
aria-label="Message input"
|
||||
|
||||
@ -13,7 +13,7 @@ const EmptyScreen = () => {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="max-w-[573px] text-center flex flex-col items-center gap-[40px]">
|
||||
<div className="flex flex-col gap-[16px]">
|
||||
<h1 className="select-none text-center text-[32px] font-bold leading-[24px] text-[var(--empty-screen-color)]">
|
||||
<h1 className="select-none text-center text-[32px] font-bold leading-[36px] text-[var(--empty-screen-color)]">
|
||||
{t("ConnectAIModel")}
|
||||
</h1>
|
||||
<p className="select-none text-center text-[16px] font-normal leading-[24px] text-[var(--empty-screen-description-color)]">
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
dialogButtonContainerStyles,
|
||||
} from "./Providers.styles";
|
||||
import { Input } from "@/components/input";
|
||||
import { Loader } from "@/components/loader";
|
||||
|
||||
type AddProviderDialogProps = {
|
||||
onClose: VoidFunction;
|
||||
@ -41,8 +42,14 @@ const AddProviderDialog = ({ onClose }: AddProviderDialogProps) => {
|
||||
url: "",
|
||||
name: "",
|
||||
});
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
const isRequestRunningRef = React.useRef(isRequestRunning);
|
||||
|
||||
const dialogRef = React.useRef<HTMLDivElement>(null);
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [buttonWidth, setButtonWidth] = React.useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue((prevValue) => ({
|
||||
@ -55,10 +62,17 @@ const AddProviderDialog = ({ onClose }: AddProviderDialogProps) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const onSubmitAction = async () => {
|
||||
const isDisabled =
|
||||
!value.name || !value.url || !!error.key || !!error.url || !!error.name;
|
||||
|
||||
const onSubmitAction = React.useCallback(async () => {
|
||||
if (isRequestRunningRef.current || isDisabled) return;
|
||||
isRequestRunningRef.current = true;
|
||||
setIsRequestRunning(true);
|
||||
|
||||
const result = await addProvider({
|
||||
type: selectedProviderInfo.type,
|
||||
name: value.name,
|
||||
name: value.name.trim(),
|
||||
key: value.key,
|
||||
baseUrl: value.url,
|
||||
});
|
||||
@ -71,21 +85,52 @@ const AddProviderDialog = ({ onClose }: AddProviderDialogProps) => {
|
||||
[result.field]: result.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
isRequestRunningRef.current = false;
|
||||
setIsRequestRunning(false);
|
||||
}, [addProvider, selectedProviderInfo, value, onClose, isDisabled]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue((prevValue) => ({
|
||||
...prevValue,
|
||||
url: selectedProviderInfo.baseUrl,
|
||||
key: "",
|
||||
}));
|
||||
setError({
|
||||
key: "",
|
||||
url: "",
|
||||
name: "",
|
||||
});
|
||||
}, [selectedProviderInfo]);
|
||||
|
||||
const isDisabled =
|
||||
!value.name || !value.url || !!error.key || !!error.url || !!error.name;
|
||||
React.useEffect(() => {
|
||||
if (buttonRef.current && buttonWidth === undefined) {
|
||||
const width = buttonRef.current.offsetWidth + 1;
|
||||
setButtonWidth(width);
|
||||
}
|
||||
}, [buttonWidth]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onSubmitAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onSubmitAction]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent header={t("AIProvider")} onClose={onClose} ref={dialogRef}>
|
||||
<DialogContent
|
||||
header={t("AddProvider")}
|
||||
onClose={onClose}
|
||||
ref={dialogRef}
|
||||
>
|
||||
<div className={dialogMainContainerStyles}>
|
||||
<div className={dialogContentContainerStyles}>
|
||||
<FieldContainer header={t("Provider")}>
|
||||
@ -106,6 +151,7 @@ const AddProviderDialog = ({ onClose }: AddProviderDialogProps) => {
|
||||
isError={!!error.name}
|
||||
placeholder={t("EnterName")}
|
||||
className="w-full"
|
||||
maxLength={128}
|
||||
/>
|
||||
</FieldContainer>
|
||||
<FieldContainer header={t("URL")} error={error.url}>
|
||||
@ -134,8 +180,17 @@ const AddProviderDialog = ({ onClose }: AddProviderDialogProps) => {
|
||||
<Button variant="default" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onSubmitAction} disabled={isDisabled}>
|
||||
{t("AddProvider")}
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={onSubmitAction}
|
||||
disabled={isDisabled || isRequestRunning}
|
||||
style={buttonWidth ? { width: `${buttonWidth}px` } : undefined}
|
||||
>
|
||||
{isRequestRunning ? (
|
||||
<Loader className="border-[var(--text-contrast-background)] border-r-transparent" />
|
||||
) : (
|
||||
t("AddProvider")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,10 +44,25 @@ const DeleteProviderDialog = ({ name, onClose }: DeleteProviderDialogProps) => {
|
||||
setProvider(provider);
|
||||
}, [providers, name]);
|
||||
|
||||
const onSubmitAction = async () => {
|
||||
const onSubmitAction = React.useCallback(async () => {
|
||||
await deleteProvider(provider);
|
||||
onClose();
|
||||
};
|
||||
}, [deleteProvider, provider, onClose]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onSubmitAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onSubmitAction]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
|
||||
@ -8,9 +8,12 @@ import { Button } from "@/components/button";
|
||||
import { FieldContainer } from "@/components/field-container";
|
||||
import { ComboBox } from "@/components/combo-box";
|
||||
import { Input } from "@/components/input";
|
||||
import { Loader } from "@/components/loader";
|
||||
|
||||
import useProviders from "@/store/useProviders";
|
||||
|
||||
import { provider as providerInstance } from "@/providers";
|
||||
|
||||
import {
|
||||
dialogMainContainerStyles,
|
||||
dialogContentContainerStyles,
|
||||
@ -25,7 +28,8 @@ type EditProviderDialogProps = {
|
||||
const EditProviderDialog = ({ name, onClose }: EditProviderDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { providers, editProvider } = useProviders();
|
||||
const { providers, editProvider, currentProvider, setCurrentProvider } =
|
||||
useProviders();
|
||||
|
||||
const [provider, setProvider] = React.useState<TProvider>(() => {
|
||||
const provider = providers.find((p) => p.name === name);
|
||||
@ -53,6 +57,13 @@ const EditProviderDialog = ({ name, onClose }: EditProviderDialogProps) => {
|
||||
url: "",
|
||||
name: "",
|
||||
});
|
||||
const [isRequestRunning, setIsRequestRunning] = React.useState(false);
|
||||
const isRequestRunningRef = React.useRef(isRequestRunning);
|
||||
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [buttonWidth, setButtonWidth] = React.useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const provider = providers.find((p) => p.name === name);
|
||||
@ -91,27 +102,6 @@ const EditProviderDialog = ({ name, onClose }: EditProviderDialogProps) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const onSubmitAction = async () => {
|
||||
const result = await editProvider(
|
||||
{
|
||||
type: provider!.type,
|
||||
name: value.name,
|
||||
key: value.key,
|
||||
baseUrl: value.url,
|
||||
},
|
||||
provider.name
|
||||
);
|
||||
|
||||
if (typeof result === "boolean" && result) {
|
||||
onClose();
|
||||
} else if (result) {
|
||||
setError((prev) => ({
|
||||
...prev,
|
||||
[result.field]: result.message,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const isSameUrlAndName =
|
||||
value.name === provider.name && value.url === provider.baseUrl;
|
||||
|
||||
@ -123,6 +113,72 @@ const EditProviderDialog = ({ name, onClose }: EditProviderDialogProps) => {
|
||||
!!error.url ||
|
||||
!!error.name;
|
||||
|
||||
const onSubmitAction = React.useCallback(async () => {
|
||||
if (isRequestRunningRef.current || isDisabled) return;
|
||||
isRequestRunningRef.current = true;
|
||||
setIsRequestRunning(true);
|
||||
|
||||
const updatedProviderInfo = {
|
||||
type: provider!.type,
|
||||
name: value.name,
|
||||
key: value.key,
|
||||
baseUrl: value.url,
|
||||
};
|
||||
|
||||
const result = await editProvider(updatedProviderInfo, provider.name);
|
||||
|
||||
if (typeof result === "boolean" && result) {
|
||||
// Check if the edited provider is the current provider
|
||||
if (currentProvider?.name === provider.name) {
|
||||
// Update current provider with new info
|
||||
const updatedProvider = {
|
||||
...provider,
|
||||
...updatedProviderInfo,
|
||||
};
|
||||
setCurrentProvider(updatedProvider);
|
||||
providerInstance.setCurrentProvider(updatedProvider);
|
||||
}
|
||||
onClose();
|
||||
} else if (result) {
|
||||
setError((prev) => ({
|
||||
...prev,
|
||||
[result.field]: result.message,
|
||||
}));
|
||||
}
|
||||
isRequestRunningRef.current = false;
|
||||
setIsRequestRunning(false);
|
||||
}, [
|
||||
isDisabled,
|
||||
editProvider,
|
||||
provider,
|
||||
value,
|
||||
onClose,
|
||||
currentProvider,
|
||||
setCurrentProvider,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (buttonRef.current && buttonWidth === undefined) {
|
||||
const width = buttonRef.current.offsetWidth + 1;
|
||||
setButtonWidth(width);
|
||||
}
|
||||
}, [buttonWidth]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onSubmitAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onSubmitAction]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent
|
||||
@ -172,8 +228,17 @@ const EditProviderDialog = ({ name, onClose }: EditProviderDialogProps) => {
|
||||
<Button variant="default" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={onSubmitAction} disabled={isDisabled}>
|
||||
{t("Save")}
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
onClick={onSubmitAction}
|
||||
disabled={isDisabled || isRequestRunning}
|
||||
style={buttonWidth ? { width: `${buttonWidth}px` } : undefined}
|
||||
>
|
||||
{isRequestRunning ? (
|
||||
<Loader className="border-[var(--text-contrast-background)] border-r-transparent" />
|
||||
) : (
|
||||
t("Save")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -24,23 +24,35 @@ const ConfigDialog = ({ open, onClose }: ConfigDialogProps) => {
|
||||
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||
const viewRef = React.useRef<EditorView | null>(null);
|
||||
const [value, setValue] = React.useState("");
|
||||
// const [error, setError] = React.useState("");
|
||||
const [error, setError] = React.useState("");
|
||||
const [isValidJson, setIsValidJson] = React.useState(true);
|
||||
|
||||
const validateJson = (jsonString: string) => {
|
||||
try {
|
||||
JSON.parse(jsonString);
|
||||
// setError("");
|
||||
setIsValidJson(true);
|
||||
return true;
|
||||
} catch {
|
||||
// setError(
|
||||
// `Invalid JSON: ${err instanceof Error ? err.message : "Unknown error"}`
|
||||
// );
|
||||
setIsValidJson(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const validateJson = React.useCallback(
|
||||
(jsonString: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
if (parsed.mcpServers) {
|
||||
setIsValidJson(true);
|
||||
setError("");
|
||||
return true;
|
||||
}
|
||||
|
||||
setIsValidJson(false);
|
||||
setError(t("ConfigurationError"));
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
setError(
|
||||
`Invalid JSON format\n ${
|
||||
err instanceof Error ? err.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
setIsValidJson(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
@ -109,17 +121,40 @@ const ConfigDialog = ({ open, onClose }: ConfigDialogProps) => {
|
||||
viewRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [open, getConfig]);
|
||||
}, [open, t, validateJson, getConfig]);
|
||||
|
||||
const onSubmitAction = React.useCallback(() => {
|
||||
if (!isValidJson) return;
|
||||
saveConfig(JSON.parse(value));
|
||||
onClose();
|
||||
}, [isValidJson, saveConfig, value, onClose]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
onSubmitAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [open, onSubmitAction]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={() => {}}>
|
||||
<DialogContent
|
||||
header={t("EditConfiguration")}
|
||||
onClose={onClose}
|
||||
className="w-[564px] h-[400px]"
|
||||
className="w-[564px] min-h-[400px]"
|
||||
>
|
||||
<div className="flex flex-col gap-[8px] h-[280px] pt-[8px] pb-[16px]">
|
||||
<div className="flex flex-col gap-[8px] h-[256px] p-[12px] bg-[var(--servers-edit-config-json-background-color)] rounded-[12px]">
|
||||
<div className="flex flex-col gap-[8px] min-h-[280px] pt-[8px] pb-[16px]">
|
||||
<div className="flex flex-col gap-[8px] min-h-[256px] p-[12px] bg-[var(--servers-edit-config-json-background-color)] rounded-[12px]">
|
||||
<div className="flex flex-row justify-between">
|
||||
<p className="font-bold text-[14px] leading-[20px] text-[var(--servers-edit-config-json-header-color)]">
|
||||
{t("EnterYourJSONConfiguration")}
|
||||
@ -132,19 +167,18 @@ const ConfigDialog = ({ open, onClose }: ConfigDialogProps) => {
|
||||
ref={editorRef}
|
||||
className="border border-[var(--servers-edit-config-json-editor-border-color)] bg-[var(--servers-edit-config-json-editor-background-color)] rounded-[4px] overflow-hidden h-full max-h-full"
|
||||
/>
|
||||
{error ? (
|
||||
<p className="text-[var(--text-negative)] font-normal text-[14px] leading-[20px] whitespace-pre-line">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-end gap-[16px] h-[64px] border-t border-[var(--servers-edit-config-buttons-border-color)] mx-[-32px] px-[32px]">
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!isValidJson}
|
||||
onClick={() => {
|
||||
saveConfig(JSON.parse(value));
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Button disabled={!isValidJson} onClick={onSubmitAction}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/dialog";
|
||||
@ -15,10 +16,25 @@ const DeleteServerDialog = ({ name, onClose }: DeleteServerDialogProps) => {
|
||||
|
||||
const { deleteCustomServer } = useServersStore();
|
||||
|
||||
const onSubmitAction = () => {
|
||||
const onSubmitAction = React.useCallback(() => {
|
||||
deleteCustomServer(name);
|
||||
onClose();
|
||||
};
|
||||
}, [deleteCustomServer, name, onClose]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onSubmitAction();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [onSubmitAction]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
|
||||
@ -25,14 +25,14 @@ const WebSearch = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveWebSearchData = () => {
|
||||
const saveWebSearchData = React.useCallback(() => {
|
||||
if (!selectedProvider || !apiKey) return;
|
||||
client.setWebSearchData({
|
||||
provider: selectedProvider,
|
||||
key: apiKey,
|
||||
});
|
||||
setSaved(true);
|
||||
};
|
||||
}, [selectedProvider, apiKey]);
|
||||
|
||||
const resetSettings = () => {
|
||||
setSelectedProvider("");
|
||||
@ -41,6 +41,21 @@ const WebSearch = () => {
|
||||
client.setWebSearchData(null);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveWebSearchData();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [saveWebSearchData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[16px] mt-[16px]">
|
||||
|
||||
@ -274,11 +274,23 @@ class AnthropicProvider
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
if (typeof error === "object" && error) {
|
||||
if ("status" in error && error.status === 401) {
|
||||
const errorMessage =
|
||||
"error" in error &&
|
||||
typeof error.error === "object" &&
|
||||
error.error &&
|
||||
"error" in error.error &&
|
||||
typeof error.error.error === "object" &&
|
||||
error.error.error &&
|
||||
"message" in error.error.error
|
||||
? error.error.error.message
|
||||
: "Invalid API key";
|
||||
|
||||
return {
|
||||
field: "key",
|
||||
message: "Invalid API key",
|
||||
message: errorMessage as string,
|
||||
};
|
||||
}
|
||||
|
||||
@ -288,14 +300,15 @@ class AnthropicProvider
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "key",
|
||||
message: "Invalid API key",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
field: "key",
|
||||
message: "Empty key",
|
||||
|
||||
@ -120,7 +120,9 @@ class Provider {
|
||||
|
||||
const title = await this.currentProvider.createChatName(message);
|
||||
|
||||
return title.slice(0, 128);
|
||||
return title.includes("</think>")
|
||||
? title.split("</think>")[1].slice(0, 128)
|
||||
: title.slice(0, 128);
|
||||
};
|
||||
|
||||
sendMessage = (
|
||||
|
||||
@ -283,7 +283,7 @@ class OpenAIProvider
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log(JSON.stringify(error));
|
||||
const errorObj = error as { code: string };
|
||||
|
||||
if (errorObj.code === "invalid_api_key") {
|
||||
@ -292,14 +292,15 @@ class OpenAIProvider
|
||||
message: "Invalid API Key",
|
||||
};
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
field: "key",
|
||||
message: "Empty key",
|
||||
@ -329,7 +330,8 @@ class OpenAIProvider
|
||||
? "GPT-5.1"
|
||||
: model.id.toUpperCase(),
|
||||
provider: "openai" as const,
|
||||
}));
|
||||
}))
|
||||
.reverse();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -281,12 +281,20 @@ class OpenRouterProvider
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || !data.apiKey) {
|
||||
if (!data.apiKey) {
|
||||
return {
|
||||
field: "key",
|
||||
message: "Empty key",
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 401 || data.apiKey) {
|
||||
return {
|
||||
field: "key",
|
||||
message: "Invalid API Key",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
@ -345,11 +353,11 @@ class OpenRouterProvider
|
||||
: model.id === "google/gemini-2.5-pro"
|
||||
? "Gemini 2.5 Pro"
|
||||
: model.id === "qwen/qwen3-235b-a22b-2507"
|
||||
? "Qwen 3"
|
||||
? "Qwen3"
|
||||
: model.id === "deepseek/deepseek-v3.1-terminus"
|
||||
? "DeepSeek V3.1 Terminus"
|
||||
: model.id === "qwen/qwen3-max"
|
||||
? "Qwen 3 Max"
|
||||
? "Qwen3 Max"
|
||||
: model.id.toUpperCase(),
|
||||
provider: "openrouter" as const,
|
||||
}));
|
||||
|
||||
@ -286,20 +286,21 @@ class TogetherProvider
|
||||
console.log(error);
|
||||
const errorObj = error as { code: string; status: number };
|
||||
|
||||
if (errorObj.status === 401) {
|
||||
if (errorObj.status === 401 || data.apiKey) {
|
||||
return {
|
||||
field: "key",
|
||||
message: "Invalid API Key",
|
||||
};
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.apiKey) {
|
||||
return {
|
||||
field: "url",
|
||||
message: "Invalid URL",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
field: "key",
|
||||
message: "Empty key",
|
||||
|
||||
@ -66,7 +66,12 @@ class WebSearch {
|
||||
livecrawl: "preferred",
|
||||
}),
|
||||
complete: function (e: { responseText: string }) {
|
||||
const data = JSON.parse(e.responseText).results;
|
||||
const parsedData = JSON.parse(e.responseText);
|
||||
const data = parsedData.error
|
||||
? {
|
||||
error: parsedData.error,
|
||||
}
|
||||
: parsedData.results;
|
||||
resolve({ data });
|
||||
},
|
||||
error: function (e: { statusCode: number }) {
|
||||
@ -80,6 +85,8 @@ class WebSearch {
|
||||
});
|
||||
});
|
||||
|
||||
console.log();
|
||||
|
||||
return JSON.stringify(result);
|
||||
} catch (e) {
|
||||
console.error("WebSearch error:", e);
|
||||
@ -108,7 +115,12 @@ class WebSearch {
|
||||
text: true,
|
||||
}),
|
||||
complete: function (e: { responseText: string }) {
|
||||
const data = JSON.parse(e.responseText).results;
|
||||
const parsedData = JSON.parse(e.responseText);
|
||||
const data = parsedData.error
|
||||
? {
|
||||
error: parsedData.error,
|
||||
}
|
||||
: parsedData.results;
|
||||
resolve({ data });
|
||||
},
|
||||
error: function (e: { statusCode: number }) {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from "@/database/threads";
|
||||
import { readMessages } from "@/database/messages";
|
||||
import type { Thread } from "@/lib/types";
|
||||
import { convertMessagesToMd } from "@/lib/utils";
|
||||
import { convertMessagesToMd, removeSpecialCharacter } from "@/lib/utils";
|
||||
|
||||
type UseThreadsStoreProps = {
|
||||
threadId: string;
|
||||
@ -71,7 +71,7 @@ const useThreadsStore = create<UseThreadsStoreProps>((set, get) => ({
|
||||
const thread = thisStore.threads.find((t) => t.threadId === id);
|
||||
const messages = await readMessages(id);
|
||||
|
||||
const title = thread?.title || "Chat Export";
|
||||
const title = removeSpecialCharacter(thread?.title || "Chat Export");
|
||||
|
||||
const content = convertMessagesToMd(messages);
|
||||
|
||||
|
||||
@ -79,5 +79,6 @@
|
||||
"EnableWebSearch": "Enable web search in settings",
|
||||
"WantDeleteServer": "This server will be deleted from your list. Continue?",
|
||||
"NoModelsAvailable": "Provider not available",
|
||||
"CheckInfo": "AI Chat can make mistakes. Check important info."
|
||||
"CheckInfo": "AI Chat can make mistakes. Check important info.",
|
||||
"ConfigurationError": "Configuration must be placed inside 'mcpServers' property"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user