feat(ee): ai chat (#2098)

* feat: ai chat

* feat: ai chat

* sync

* cleanup

* view space button
This commit is contained in:
Philip Okugbe
2026-04-10 19:23:47 +01:00
committed by GitHub
parent da9b43681e
commit 57efb91bd3
63 changed files with 4149 additions and 48 deletions
@@ -62,7 +62,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
query: props.query,
includeUsers: true,
includePages: true,
spaceId: space.id,
spaceId: space?.id,
limit: props.query ? 10 : 5,
preload: true,
});
@@ -53,8 +53,8 @@ const mentionRenderItems = () => {
const editorDom = props.editor?.view?.dom;
const asideEl = editorDom?.closest(".mantine-AppShell-aside");
const dialogEl = editorDom?.closest("[data-comment-dialog]");
const isInCommentContext = !!(asideEl || dialogEl);
// const isInCommentContext = !!asideEl;
const chatInput = editorDom?.closest("[data-chat-input]");
const isInCommentContext = !!(asideEl || dialogEl || chatInput);
component = new ReactRenderer(MentionList, {
props: { ...props, isInCommentContext },
@@ -0,0 +1,28 @@
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md) var(--mantine-spacing-lg);
}
.heading {
font-size: 1.75rem;
font-weight: 600;
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
text-align: center;
margin: 0;
line-height: 1.2;
}
.subtitle {
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
text-align: center;
margin-top: 6px;
margin-bottom: var(--mantine-spacing-lg);
}
.inputContainer {
width: 100%;
max-width: 640px;
}
@@ -0,0 +1,60 @@
import { useAtomValue } from "jotai";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import ChatInput from "@/ee/ai-chat/components/chat-input";
import type {
ChatAttachment,
PageMention,
} from "@/ee/ai-chat/types/ai-chat.types";
import classes from "./home-ai-prompt.module.css";
export type HomeAiPromptInitialState = {
initialContent: string;
initialMentions: PageMention[];
initialAttachments: ChatAttachment[];
};
export default function HomeAiPrompt() {
const { t } = useTranslation();
const navigate = useNavigate();
const workspace = useAtomValue(workspaceAtom);
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
if (!aiChatEnabled) return null;
const handleSend = (
content: string,
mentions: PageMention[],
attachments: ChatAttachment[],
) => {
if (!content.trim() && attachments.length === 0) return;
const state: HomeAiPromptInitialState = {
initialContent: content,
initialMentions: mentions,
initialAttachments: attachments,
};
navigate("/ai", { state });
};
return (
<div className={classes.wrapper}>
<h1 className={classes.heading}>
{t("Welcome to {{name}}", { name: workspace?.name ?? "Docmost" })}
</h1>
<div className={classes.subtitle}>
{t("Ask anything or search your workspace")}
</div>
<div className={classes.inputContainer}>
<ChatInput
isStreaming={false}
onSend={handleSend}
onStop={() => {}}
placeholder={t("Ask anything... Use @ to mention pages")}
autofocus={false}
/>
</div>
</div>
);
}
@@ -29,6 +29,8 @@
border-radius: var(--mantine-radius-sm);
border: 1px solid;
font-weight: bold;
white-space: nowrap;
flex-shrink: 0;
@mixin light {
color: var(--mantine-color-gray-7);
@@ -0,0 +1,22 @@
.card {
background-color: var(--mantine-color-body);
width: 220px;
@mixin hover {
box-shadow: var(--mantine-shadow-xs);
transform: scale(1.02);
}
}
.cardSection {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
}
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
@@ -0,0 +1,77 @@
import { Text, Card, rem, Group, Button } from "@mantine/core";
import {
prefetchSpace,
useGetSpacesQuery,
} from "@/features/space/queries/space-query.ts";
import { getSpaceUrl } from "@/lib/config.ts";
import { Link } from "react-router-dom";
import classes from "./space-carousel.module.css";
import { formatMemberCount } from "@/lib";
import { useTranslation } from "react-i18next";
import { IconArrowRight } from "@tabler/icons-react";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import CardCarousel from "@/components/ui/card-carousel";
export default function SpaceCarousel() {
const { t } = useTranslation();
const { data } = useGetSpacesQuery({ limit: 20 });
const cards = data?.items.map((space) => (
<Card
key={space.id}
p="xs"
radius="md"
component={Link}
to={getSpaceUrl(space.slug)}
onMouseEnter={() => prefetchSpace(space.slug, space.id)}
className={classes.card}
withBorder
>
<Card.Section className={classes.cardSection} h={40}></Card.Section>
<CustomAvatar
name={space.name}
avatarUrl={space.logo}
type={AvatarIconType.SPACE_ICON}
color="initials"
variant="filled"
size="md"
mt={rem(-20)}
/>
<Text fz="md" fw={500} mt="xs" className={classes.title}>
{space.name}
</Text>
<Text c="dimmed" size="xs" fw={700} mt="md">
{formatMemberCount(space.memberCount, t)}
</Text>
</Card>
));
return (
<>
<Group justify="space-between" align="center" mb="md">
<Text fz="sm" fw={500}>
{t("Spaces you belong to")}
</Text>
</Group>
<CardCarousel ariaLabel={t("Spaces you belong to")}>{cards}</CardCarousel>
{data?.items && data.items.length > 1 && (
<Group justify="flex-end" mt="lg">
<Button
component={Link}
to="/spaces"
variant="subtle"
rightSection={<IconArrowRight size={16} />}
size="sm"
>
{t("View all spaces")}
</Button>
</Group>
)}
</>
);
}
@@ -43,6 +43,7 @@ export interface IWorkspaceAiSettings {
search?: boolean;
generative?: boolean;
mcp?: boolean;
chat?: boolean;
}
export interface IWorkspaceSharingSettings {