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:
Oleg Korshul
2025-11-21 09:13:21 +00:00
23 changed files with 519 additions and 217 deletions

View File

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

View File

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

View File

@ -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" ||

View File

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

View File

@ -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) => {

View File

@ -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"

View File

@ -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)]">

View File

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

View File

@ -44,11 +44,26 @@ 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}>
<DialogContent header={t("Warning")} onClose={onClose} withWarningIcon>

View File

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

View File

@ -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) => {
const validateJson = React.useCallback(
(jsonString: string) => {
try {
JSON.parse(jsonString);
// setError("");
const parsed = JSON.parse(jsonString);
if (parsed.mcpServers) {
setIsValidJson(true);
setError("");
return true;
} catch {
// setError(
// `Invalid JSON: ${err instanceof Error ? err.message : "Unknown error"}`
// );
}
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>

View File

@ -1,3 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Dialog, DialogContent } from "@/components/dialog";
@ -15,11 +16,26 @@ 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}>
<DialogContent header={t("Warning")} onClose={onClose} withWarningIcon>

View File

@ -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]">

View File

@ -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",
field: "key",
message: "Invalid API key",
};
}
}
return {
field: "key",
message: "Empty key",

View File

@ -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 = (

View File

@ -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,6 +292,7 @@ class OpenAIProvider
message: "Invalid API Key",
};
}
}
if (data.apiKey) {
return {
@ -299,7 +300,7 @@ class OpenAIProvider
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();
};
}

View File

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

View File

@ -286,12 +286,13 @@ 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 {
@ -299,7 +300,7 @@ class TogetherProvider
message: "Invalid URL",
};
}
}
return {
field: "key",
message: "Empty key",

View File

@ -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 }) {

View File

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

View File

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