Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'fumadocs-ui/style.css';
import 'fumadocs-ui/components/image-zoom2.css';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import Script from 'next/script';
import { Analytics } from '@vercel/analytics/next';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { CovenSearchDialog } from '@/components/search-dialog';
Expand Down Expand Up @@ -72,18 +71,12 @@ export default function Layout({ children }: { children: ReactNode }) {
}}
>
{children}
{/* Shared launcher: Ask Salem chat + OpenCoven Feedback panel.
The feedback SDK is lazy-loaded on first use (lib/feedback-widget). */}
<AskSalem />
</RootProvider>
<Analytics />
<SpeedInsights />
{/* OpenCoven Feedback widget — loads anonymously; identify() called per-page when user is known */}
<Script
id="opencoven-feedback"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(function(w,d){if(w.OpenCovenFeedback)return;w.OpenCovenFeedback=function(){(w.OpenCovenFeedback.q=w.OpenCovenFeedback.q||[]).push(arguments)};var s=d.createElement('script');s.async=true;s.src='https://feedback.opencoven.ai/api/widget/sdk.js';d.head.appendChild(s)})(window,document);OpenCovenFeedback('init');`,
}}
/>
</body>
</html>
);
Expand Down
58 changes: 39 additions & 19 deletions components/ask-salem.module.css
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
.launcher {
.launcherGroup {
position: fixed;
bottom: 16px;
left: 16px;
z-index: 60;
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.6rem 1rem;
align-items: stretch;
border: 1px solid var(--color-fd-border);
border-radius: 999px;
background: var(--color-fd-background);
color: var(--color-fd-foreground);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.18);
transition: border-color 0.15s ease, transform 0.15s ease;
overflow: hidden;
transition: border-color 0.15s ease;
}

/* On viewports where the docs sidebar shows, clear its footer row
(search shortcut + theme toggle) instead of covering it. Applied only on
/docs routes via .overSidebar. */
/docs routes via .overSidebar — and dropped while the feedback panel is
open, since that panel sits at bottom: 88px in the same corner. */
@media (min-width: 768px) {
.launcher {
.launcherGroup {
left: 24px;
}

.launcher.overSidebar {
.launcherGroup.overSidebar {
bottom: 78px;
}
}

.launcher:hover {
.launcherGroup:hover {
border-color: var(--coven-violet);
transform: translateY(-1px);
}

.launcherSegment {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.6rem 0.9rem;
border: none;
background: transparent;
color: var(--color-fd-foreground);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
}

.launcherSegment:hover,
.launcherSegment.launcherSegmentActive {
background: color-mix(in srgb, var(--coven-violet) 10%, transparent);
}
Comment on lines +35 to +52

.launcherDivider {
width: 1px;
align-self: stretch;
flex-shrink: 0;
background: var(--color-fd-border);
}

.panel {
Expand Down Expand Up @@ -61,11 +83,6 @@
width: auto;
max-height: calc(100dvh - 16px);
}

/* Keep the footer clear of the feedback launcher pinned bottom-right. */
.footer {
padding-right: 64px;
}
}

.header {
Expand Down Expand Up @@ -107,6 +124,7 @@
margin-top: 1px;
}

.headerAction,
.headerClose {
display: inline-flex;
align-items: center;
Expand All @@ -120,6 +138,7 @@
cursor: pointer;
}

.headerAction:hover,
.headerClose:hover {
background: var(--color-fd-accent);
color: var(--color-fd-foreground);
Expand Down Expand Up @@ -268,7 +287,8 @@
animation: none;
}

.launcher {
.launcherGroup,
.launcherSegment {
transition: none;
}
}
Expand Down
110 changes: 99 additions & 11 deletions components/ask-salem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
// admin password upstream, so every question here is sent standalone (the
// conversation log is client-side only). Answers are rendered from a minimal
// markdown subset using React elements — never innerHTML.
//
// The launcher is shared with the OpenCoven Feedback widget: one pill,
// bottom-left, with an "Ask Salem" segment (this chat panel) and a
// "Feedback" segment (the feedback panel from feedback.opencoven.ai,
// mounted with its own launcher disabled — see lib/feedback-widget). The
// two panels are mutually exclusive.

import {
useCallback,
Expand All @@ -27,6 +33,12 @@ import {
CLIENT_MARKER_HEADER,
CLIENT_MARKER_VALUE,
} from '@/lib/ask-salem-guards';
import {
FEEDBACK_PORTAL,
closeFeedback,
onFeedbackEvent,
openFeedback,
} from '@/lib/feedback-widget';
import s from './ask-salem.module.css';

const PRODUCTION_ORIGIN = 'https://docs.opencoven.ai';
Expand Down Expand Up @@ -160,6 +172,7 @@ function parseRetryAfterSec(res: Response): number {

export function AskSalem() {
const [open, setOpen] = useState(false);
const [feedbackOpen, setFeedbackOpen] = useState(false);
const pathname = usePathname();
// Docs pages pin a sidebar footer bottom-left; lift the launcher above it.
const overSidebar = pathname?.startsWith('/docs') ?? false;
Expand Down Expand Up @@ -198,14 +211,49 @@ export function AskSalem() {
}, [log]);

const close = useCallback(() => setOpen(false), []);

// The feedback panel is closable from inside its own iframe (close button,
// mobile backdrop), so mirror the SDK's open/close events into local state.
useEffect(() => {
if (!open) return;
const offOpen = onFeedbackEvent('open', () => setFeedbackOpen(true));
const offClose = onFeedbackEvent('close', () => setFeedbackOpen(false));
return () => {
offOpen();
offClose();
};
}, []);

const openSalem = useCallback(() => {
closeFeedback();
setFeedbackOpen(false);
setOpen(true);
}, []);

const showFeedback = useCallback(() => {
setOpen(false);
setFeedbackOpen(true);
openFeedback().catch(() => {
// SDK blocked or unreachable — fall back to the public portal.
setFeedbackOpen(false);
window.open(FEEDBACK_PORTAL, '_blank', 'noopener,noreferrer');
});
}, []);

const hideFeedback = useCallback(() => {
closeFeedback();
setFeedbackOpen(false);
}, []);

useEffect(() => {
if (!open && !feedbackOpen) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') close();
if (event.key !== 'Escape') return;
if (open) close();
if (feedbackOpen) hideFeedback();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, close]);
}, [open, feedbackOpen, close, hideFeedback]);

const appendAssistant = useCallback((content: string) => {
setLog((turns) => {
Expand Down Expand Up @@ -311,16 +359,47 @@ export function AskSalem() {
const blocked = streaming || cooldownSec > 0;

if (!open) {
// The feedback panel sits at bottom: 88px in this corner, so while it is
// open the sidebar lift is dropped to keep the launcher clear of it.
const lifted = overSidebar && !feedbackOpen;
return (
<button
type="button"
className={overSidebar ? `${s.launcher} ${s.overSidebar}` : s.launcher}
onClick={() => setOpen(true)}
aria-label="Ask Salem, the OpenCoven documentation assistant"
<div
className={lifted ? `${s.launcherGroup} ${s.overSidebar}` : s.launcherGroup}
role="group"
aria-label="Documentation assistant and feedback"
>
<Icon icon="ph:moon-stars-duotone" width={16} color="var(--coven-violet)" aria-hidden="true" />
Ask Salem
</button>
<button
type="button"
className={s.launcherSegment}
onClick={openSalem}
aria-label="Ask Salem, the OpenCoven documentation assistant"
>
<Icon icon="ph:moon-stars-duotone" width={16} color="var(--coven-violet)" aria-hidden="true" />
Ask Salem
</button>
<span className={s.launcherDivider} aria-hidden="true" />
<button
type="button"
className={
feedbackOpen
? `${s.launcherSegment} ${s.launcherSegmentActive}`
: s.launcherSegment
}
onClick={feedbackOpen ? hideFeedback : showFeedback}
aria-expanded={feedbackOpen}
aria-label={
feedbackOpen ? 'Close feedback panel' : 'Open feedback panel'
}
>
<Icon
icon={feedbackOpen ? 'ph:x-bold' : 'ph:chat-circle-dots-duotone'}
width={16}
color="var(--coven-violet)"
aria-hidden="true"
/>
Feedback
</button>
</div>
);
}

Expand All @@ -338,6 +417,15 @@ export function AskSalem() {
<div className={s.headerTitle}>Ask Salem</div>
<div className={s.headerSub}>Searches these docs · answers may be imperfect</div>
</div>
<button
type="button"
className={s.headerAction}
onClick={showFeedback}
aria-label="Switch to the feedback panel"
title="Send feedback"
>
<Icon icon="ph:chat-circle-dots-duotone" width={15} aria-hidden="true" />
</button>
<button
type="button"
className={s.headerClose}
Expand Down
33 changes: 0 additions & 33 deletions components/widget-identify.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions content/docs/reference/ask-salem.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ read_when:

Every page on this site includes **Ask Salem**, a floating assistant backed by [salem.opencoven.ai](https://salem.opencoven.ai) — a retrieval service that indexes this documentation and answers questions with linked sources. No account or token is required.

Salem shares its launcher with the site's [feedback widget](/docs/reference/feedback-widget): one pill, bottom-left, with an **Ask Salem** segment that opens the chat panel and a **Feedback** segment that opens the OpenCoven Feedback panel. The two panels are mutually exclusive, and the feedback SDK is loaded lazily the first time the Feedback segment is used.

## Transport paths

The widget picks its transport by origin:
Expand Down
2 changes: 2 additions & 0 deletions content/docs/reference/feedback-widget.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ OpenCovenFeedback('logout')

To restrict the widget so only your own users can submit feedback, use the SSO token shape and call `init` with `launcher: false`. Then mount the launcher yourself once identity resolves.

> `launcher: false` is also the hook for replacing the floating bubble with your own UI: call `open`/`close` from any element you control. This docs site does exactly that — its bottom-left launcher is shared with the [Ask Salem assistant](/docs/reference/ask-salem), and the Feedback segment drives the widget through `init({ launcher: false, placement: 'left' })` plus `open`.
Comment on lines 72 to +74

```ts
// 1. Load without showing the launcher
OpenCovenFeedback('init', { launcher: false })
Expand Down
Loading