@@ -100,8 +118,16 @@ export default function ChatMessage({
);
}
+ // Only label the article when there's something meaningful to announce.
+ // Tool-only assistant turns (no text) shouldn't announce "Assistant said:" with empty content.
+ const hasAnnouncableContent = Boolean(content);
+
return (
-
+
{toolCalls && toolCalls.length > 0 && (
@@ -131,7 +157,10 @@ export default function ChatMessage({
{!isStreaming && message.content && (
-
+
)}
diff --git a/apps/client/src/ee/ai-chat/styles/ai-chat.module.css b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css
index 307669071..2daa0a2b8 100644
--- a/apps/client/src/ee/ai-chat/styles/ai-chat.module.css
+++ b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css
@@ -76,7 +76,6 @@
padding: var(--mantine-spacing-xs) var(--mantine-spacing-lg) var(--mantine-spacing-lg);
}
-/* Empty state - Notion AI style centered layout */
.emptyState {
flex: 1;
display: flex;
@@ -106,6 +105,7 @@
font-size: 1.5rem;
font-weight: 600;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
+ margin-top: 0;
margin-bottom: var(--mantine-spacing-xl);
text-align: center;
}
@@ -128,6 +128,7 @@
color: var(--mantine-color-dimmed);
text-transform: uppercase;
letter-spacing: 0.5px;
+ margin-top: 0;
margin-bottom: var(--mantine-spacing-sm);
}
diff --git a/apps/client/src/ee/ai-chat/styles/chat-input.module.css b/apps/client/src/ee/ai-chat/styles/chat-input.module.css
index da8d1546e..1401c2c10 100644
--- a/apps/client/src/ee/ai-chat/styles/chat-input.module.css
+++ b/apps/client/src/ee/ai-chat/styles/chat-input.module.css
@@ -114,7 +114,7 @@
}
:global(.ProseMirror p.is-editor-empty:first-child::before) {
- color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
+ color: var(--mantine-color-placeholder);
content: attr(data-placeholder);
float: left;
height: 0;
@@ -183,7 +183,7 @@
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
background: none;
cursor: pointer;
- color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
+ color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
transition: color 150ms, background-color 150ms;
@mixin hover {
diff --git a/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css b/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css
index 62dcb29f6..f344b6cd8 100644
--- a/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css
+++ b/apps/client/src/ee/ai-chat/styles/chat-sidebar.module.css
@@ -15,6 +15,7 @@
}
.title {
+ margin: 0;
font-weight: 600;
font-size: var(--mantine-font-size-sm);
}
@@ -33,6 +34,7 @@
}
.chatGroupLabel {
+ margin: 0;
padding: 4px var(--mantine-spacing-xs);
font-size: var(--mantine-font-size-xs);
font-weight: 600;
@@ -118,7 +120,8 @@
color: inherit;
}
-.chatItem:hover .chatItemDate {
+.chatItem:hover .chatItemDate,
+.chatItem:focus-within .chatItemDate {
opacity: 0;
}
@@ -133,6 +136,12 @@
position: relative;
}
-.chatItem:hover .chatItemActions {
+.chatItem:hover .chatItemActions,
+.chatItem:focus-within .chatItemActions {
opacity: 1;
}
+
+.chatItemActions :global(.mantine-ActionIcon-root):focus-visible {
+ outline: 2px solid var(--mantine-primary-color-filled);
+ outline-offset: 2px;
+}
diff --git a/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx b/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx
index 1c1cd7e18..f9fa2df3e 100644
--- a/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx
+++ b/apps/client/src/ee/ai/components/editor/ai-menu/ai-menu.tsx
@@ -1,7 +1,14 @@
import { Editor } from "@tiptap/react";
import { ActionIcon, TextInput } from "@mantine/core";
import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type JSX,
+} from "react";
import { createPortal } from "react-dom";
import { useAtom } from "jotai";
import { IconArrowUp } from "@tabler/icons-react";
@@ -14,7 +21,7 @@ import { ResultPreview } from "./result-preview.tsx";
import classes from "./ai-menu.module.css";
import { marked } from "marked";
import { DOMSerializer } from "@tiptap/pm/model";
-import { copyToClipboard, htmlToMarkdown } from "@docmost/editor-ext";
+import { copyToClipboard, htmlToMarkdown, isEditorReady } from "@docmost/editor-ext";
import { useLocation } from "react-router-dom";
interface EditorAiMenuProps {
@@ -49,7 +56,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
});
}, [prompt, output, activeCommandSet]);
const updateMenuPlacement = useCallback(() => {
- if (!editor || !showAiMenu) return;
+ if (!isEditorReady(editor) || !showAiMenu) return;
const { view } = editor;
const { from, to } = editor.state.selection;
@@ -95,7 +102,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
);
const handleGenerate = useCallback(
(item?: CommandItem) => {
- if (!editor || isLoading) return;
+ if (!isEditorReady(editor) || isLoading) return;
let command: CommandItem | null = item || null;
@@ -158,6 +165,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
return setActiveCommandSet("main");
}
if (item.id === "result-replace") {
+ if (!isEditorReady(editor)) return setShowAiMenu(false);
const chain = editor.chain().focus();
if (lastAction.action === AiAction.CONTINUE_WRITING) {
@@ -183,6 +191,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
return setShowAiMenu(false);
}
if (item.id === "result-insert-below") {
+ if (!isEditorReady(editor)) return setShowAiMenu(false);
editor
.chain()
.focus()
@@ -246,7 +255,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
);
useEffect(() => {
- if (!editor) return;
+ if (!isEditorReady(editor)) return;
const handleClose = () => setShowAiMenu(false);
const observer = new ResizeObserver(() => {
diff --git a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx
index ed44811a7..e787c0d48 100644
--- a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx
+++ b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx
@@ -33,6 +33,7 @@ export function ApiKeyCreatedModal({
onClose={onClose}
title={t("{{credential}} created", { credential: t("API key") })}
size="lg"
+ closeButtonProps={{ "aria-label": t("Close") }}
>
{
if (!date) return t("Never");
- return format(new Date(date), "MMM dd, yyyy");
+ return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
};
const isExpired = (expiresAt: string | null) => {
diff --git a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx
index 0b93c080b..53341a614 100644
--- a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx
+++ b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx
@@ -31,7 +31,7 @@ export function CreateApiKeyModal({
onClose,
onSuccess,
}: CreateApiKeyModalProps) {
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
const [expirationOption, setExpirationOption] = useState("30");
const createApiKeyMutation = useCreateApiKeyMutation();
@@ -59,7 +59,7 @@ export function CreateApiKeyModal({
const getExpirationLabel = (days: number) => {
const date = new Date();
date.setDate(date.getDate() + days);
- const formatted = date.toLocaleDateString("en-US", {
+ const formatted = date.toLocaleDateString(i18n.language, {
month: "short",
day: "2-digit",
year: "numeric",
@@ -107,6 +107,7 @@ export function CreateApiKeyModal({
onClose={handleClose}
title={t("Create {{credential}}", { credential: t("API key") })}
size="md"
+ closeButtonProps={{ "aria-label": t("Close") }}
>