feat: add temporary sidebar share opinion prompt

Adds a dialog with call booking and survey actions for user feedback
Shows a one-time sidebar toast prompting users to share what’s useful or missing
Adds localized copy for the new feedback entry points
This commit is contained in:
Bohdan Triapitsyn
2026-07-06 00:37:24 +03:00
parent a6edc7baee
commit 086982c1b3
14 changed files with 188 additions and 1 deletions
@@ -0,0 +1,65 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useI18n } from '@/lib/i18n';
import { openExternalUrl } from '@/lib/url';
type ShareOpinionDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
const SHARE_OPINION_MARKDOWN = `**Help shape what OpenChamber becomes next!**
Hey 👋,
OpenChamber has grown mostly through word of mouth, GitHub issues, Discord feedback, and people telling me what is broken, confusing, or surprisingly useful.
I'm planning the next chapter now - mobile, better remote access, a tighter VS Code ↔ desktop/web/mobile flow, and more, but before building too much, I want to hear from the people actually using it.
I'm doing a round of short 1-on-1 calls. No sales pitch, no formal script - just a real conversation about how you use OpenChamber, what you love, what frustrates you, and what would make it much more valuable.
You can book a call or use the short survey below.
What you get:
- A direct chance to influence the roadmap
- Your pain points and feature requests prioritized with more context
- A Power User role in Discord for people helping shape the product
- My genuine thanks for helping make OpenChamber better
**This project is what it is because of your feedback. Thank you, genuinely.**
I'll remove this button in two weeks 🙂`;
export function ShareOpinionDialog({ open, onOpenChange }: ShareOpinionDialogProps): React.ReactNode {
const { t } = useI18n();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
<DialogTitle className="sr-only">{t('shareOpinion.dialog.title')}</DialogTitle>
<SimpleMarkdownRenderer content={SHARE_OPINION_MARKDOWN} className="typography-markdown-body" enableFileReferences={false} />
<div className="mt-4 flex flex-wrap gap-2">
<button
type="button"
className="inline-flex items-center gap-2 rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-4 py-2.5 typography-ui-label text-foreground hover:bg-[var(--interactive-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]"
onClick={() => void openExternalUrl('https://calendly.com/artmore/30min')}
>
<Icon name="video-chat" className="size-5 text-[var(--status-warning)]" />
{t('shareOpinion.actions.bookCall')}
</button>
<button
type="button"
className="inline-flex items-center gap-2 rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-4 py-2.5 typography-ui-label text-foreground hover:bg-[var(--interactive-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]"
onClick={() => void openExternalUrl('https://forms.gle/cdMVKUGs5QuLWkA86')}
>
<Icon name="survey" className="size-5 text-[var(--pr-merged)]" />
{t('shareOpinion.actions.shortSurvey')}
</button>
</div>
</DialogContent>
</Dialog>
);
}
@@ -227,6 +227,7 @@ export const iconSpriteData = {
"unpin": `<path d="M20.9701 17.1716 19.5559 18.5858 16.0214 15.0513 15.9476 15.1251 15.2405 18.6606 13.8263 20.0748 9.58369 15.8322 4.63394 20.7819 3.21973 19.3677 8.16947 14.418 3.92683 10.1753 5.34105 8.7611 8.87658 8.05399 8.95029 7.98028 5.41373 4.44371 6.82794 3.0295 20.9701 17.1716ZM10.3645 9.39449 9.86261 9.8964 7.04072 10.4608 13.5409 16.9609 14.1052 14.139 14.6071 13.6371 10.3645 9.39449ZM18.7761 9.46821 17.4356 10.8087 18.8498 12.2229 20.1903 10.8824 20.8974 11.5895 22.3116 10.1753 13.8263 1.69003 12.4121 3.10425 13.1192 3.81135 11.7787 5.15185 13.1929 6.56607 14.5334 5.22557 18.7761 9.46821Z" fill="currentColor"/>`,
"user-3": `<path d="M20 22H18V20C18 18.3431 16.6569 17 15 17H9C7.34315 17 6 18.3431 6 20V22H4V20C4 17.2386 6.23858 15 9 15H15C17.7614 15 20 17.2386 20 20V22ZM12 13C8.68629 13 6 10.3137 6 7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7C18 10.3137 15.3137 13 12 13ZM12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" fill="currentColor"/>`,
"user": `<path d="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H18C18 18.6863 15.3137 16 12 16C8.68629 16 6 18.6863 6 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM12 11C14.21 11 16 9.21 16 7C16 4.79 14.21 3 12 3C9.79 3 8 4.79 8 7C8 9.21 9.79 11 12 11Z" fill="currentColor"/>`,
"video-chat": `<path d="M14 10.25L17 8V14L14 11.75V14H7V8H14V10.25ZM5.76282 17H20V5H4V18.3851L5.76282 17ZM6.45455 19L2 22.5V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.45455Z" fill="currentColor"/>`,
"volume-up": `<path d="M6.60282 10.0001L10 7.22056V16.7796L6.60282 14.0001H3V10.0001H6.60282ZM2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z" fill="currentColor"/>`,
"window": `<path d="M21 3C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM20 11H4V19H20V11ZM20 5H4V9H20V5ZM11 6V8H9V6H11ZM7 6V8H5V6H7Z" fill="currentColor"/>`,
} as const satisfies Record<string, string>;
@@ -35,6 +35,7 @@ import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders
import { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { ShareOpinionDialog } from '@/components/feedback/ShareOpinionDialog';
import { SessionGroupSection } from './sidebar/SessionGroupSection';
import { SidebarHeader } from './sidebar/SidebarHeader';
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
@@ -80,6 +81,7 @@ import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
const SHARE_OPINION_TOAST_STORAGE_KEY = 'openchamber.shareOpinionToast.dismissed.v2';
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
const GROUP_ORDER_STORAGE_KEY = 'oc.sessions.groupOrder';
const GROUP_COLLAPSE_STORAGE_KEY = 'oc.sessions.groupCollapse';
@@ -196,6 +198,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const newWorktreeDialogOpen = useUIStore((state) => state.isNewWorktreeDialogOpen);
const setNewWorktreeDialogOpen = useUIStore((state) => state.setNewWorktreeDialogOpen);
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const [shareOpinionDialogOpen, setShareOpinionDialogOpen] = React.useState(false);
const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState<string | null>(null);
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
const [renameFolderDraft, setRenameFolderDraft] = React.useState('');
@@ -635,6 +638,36 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
});
}, [t, updateStore]);
const handleOpenShareOpinionDialog = React.useCallback(() => {
setShareOpinionDialogOpen(true);
}, []);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
if (window.localStorage.getItem(SHARE_OPINION_TOAST_STORAGE_KEY) === 'true') {
return;
}
window.localStorage.setItem(SHARE_OPINION_TOAST_STORAGE_KEY, 'true');
} catch {
// If storage is unavailable, still show once for this sidebar mount.
}
const timeoutId = window.setTimeout(() => {
toast.info(t('shareOpinion.toast.title'), {
description: t('shareOpinion.toast.description'),
action: {
label: t('shareOpinion.actions.shareOpinion'),
onClick: () => setShareOpinionDialogOpen(true),
},
duration: 12_000,
});
}, 1_000);
return () => window.clearTimeout(timeoutId);
}, [t]);
const handleOpenSettings = React.useCallback(() => {
if (mobileVariant) {
setSessionSwitcherOpen(false);
@@ -1619,10 +1652,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
onOpenShortcuts={toggleHelpDialog}
onOpenAbout={() => setAboutDialogOpen(true)}
onOpenUpdate={handleOpenUpdateDialog}
onOpenShareOpinion={handleOpenShareOpinionDialog}
showRuntimeButtons={!isVSCode}
showUpdateButton={showSidebarUpdateButton}
/>
<ShareOpinionDialog
open={shareOpinionDialogOpen}
onOpenChange={setShareOpinionDialogOpen}
/>
<UpdateDialog
open={updateDialogOpen}
onOpenChange={setUpdateDialogOpen}
@@ -9,6 +9,7 @@ type Props = {
onOpenShortcuts: () => void;
onOpenAbout: () => void;
onOpenUpdate: () => void;
onOpenShareOpinion: () => void;
showRuntimeButtons?: boolean;
showUpdateButton?: boolean;
};
@@ -20,6 +21,7 @@ export function SidebarFooter({
onOpenShortcuts,
onOpenAbout,
onOpenUpdate,
onOpenShareOpinion,
showRuntimeButtons = true,
showUpdateButton = true,
}: Props): React.ReactNode {
@@ -65,7 +67,17 @@ export function SidebarFooter({
>
{t('sessions.sidebar.footer.actions.update')}
</Button>
) : null}
) : (
<Button
type="button"
variant="default"
size="xs"
className="ml-auto border-[var(--primary-base)]/15 bg-[var(--primary-base)]/5 text-[var(--primary-base)]/60 hover:bg-[var(--primary-base)]/10 hover:text-[var(--primary-base)]/75"
onClick={onOpenShareOpinion}
>
{t('sessions.sidebar.footer.actions.shareOpinion')}
</Button>
)}
</div>
);
}
+7
View File
@@ -22,6 +22,13 @@ export const dict = {
'pwa.installPrompt.dismiss': 'Dismiss',
'pwa.installPrompt.started': 'Install started',
'pwa.installPrompt.installed': 'OpenChamber installed',
'shareOpinion.actions.shareOpinion': 'Share opinion',
'shareOpinion.actions.bookCall': 'Book a call',
'shareOpinion.actions.shortSurvey': 'Short survey',
'shareOpinion.dialog.title': 'Share your opinion',
'shareOpinion.toast.title': 'Help shape OpenChamber',
'shareOpinion.toast.description': 'Share what is useful, confusing, or missing to help shape what comes next.',
'sessions.sidebar.footer.actions.shareOpinion': 'Share opinion',
'layout.mainTab.chat': 'Chat',
'layout.mainTab.plan': 'Plan',
'layout.mainTab.diff': 'Diff',
+7
View File
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
"pwa.installPrompt.dismiss": "Descartar",
"pwa.installPrompt.started": "Instalación iniciada",
"pwa.installPrompt.installed": "OpenChamber se instaló",
"shareOpinion.actions.shareOpinion": "Compartir opinión",
"shareOpinion.actions.bookCall": "Reservar llamada",
"shareOpinion.actions.shortSurvey": "Encuesta breve",
"shareOpinion.dialog.title": "Comparte tu opinión",
"shareOpinion.toast.title": "Ayuda a dar forma a OpenChamber",
"shareOpinion.toast.description": "Cuenta qué es útil, confuso o falta para ayudar a definir lo que viene después.",
"sessions.sidebar.footer.actions.shareOpinion": "Compartir opinión",
"layout.mainTab.chat": "Chat",
"layout.mainTab.plan": "Plan",
"layout.mainTab.diff": "Diff",
+7
View File
@@ -22,6 +22,13 @@ export const dict = {
'pwa.installPrompt.dismiss': 'Ignorer',
'pwa.installPrompt.started': 'Installation démarrée',
'pwa.installPrompt.installed': 'OpenChamber installé',
'shareOpinion.actions.shareOpinion': 'Donner son avis',
'shareOpinion.actions.bookCall': 'Réserver un appel',
'shareOpinion.actions.shortSurvey': 'Court sondage',
'shareOpinion.dialog.title': 'Donnez votre avis',
'shareOpinion.toast.title': 'Aidez à façonner OpenChamber',
'shareOpinion.toast.description': 'Dites ce qui est utile, confus ou manquant pour aider à définir la suite.',
'sessions.sidebar.footer.actions.shareOpinion': 'Donner son avis',
'layout.mainTab.chat': 'Chat',
'layout.mainTab.plan': 'Plan',
'layout.mainTab.diff': 'Diff',
+7
View File
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
'pwa.installPrompt.dismiss': '閉じる',
'pwa.installPrompt.started': 'インストールを開始しました',
'pwa.installPrompt.installed': 'OpenChamberをインストールしました',
'shareOpinion.actions.shareOpinion': '意見を共有',
'shareOpinion.actions.bookCall': '通話を予約',
'shareOpinion.actions.shortSurvey': '短いアンケート',
'shareOpinion.dialog.title': 'ご意見をお聞かせください',
'shareOpinion.toast.title': 'OpenChamber の次を一緒に作る',
'shareOpinion.toast.description': '便利な点、分かりにくい点、足りない点を教えて、次の方向づくりに参加してください。',
'sessions.sidebar.footer.actions.shareOpinion': '意見を共有',
'layout.mainTab.chat': 'チャット',
'layout.mainTab.plan': '計画',
'layout.mainTab.diff': '差分',
+7
View File
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
'pwa.installPrompt.dismiss': '닫기',
'pwa.installPrompt.started': '설치가 시작되었습니다',
'pwa.installPrompt.installed': 'OpenChamber가 설치되었습니다',
'shareOpinion.actions.shareOpinion': '의견 공유',
'shareOpinion.actions.bookCall': '통화 예약',
'shareOpinion.actions.shortSurvey': '짧은 설문',
'shareOpinion.dialog.title': '의견을 공유해 주세요',
'shareOpinion.toast.title': 'OpenChamber의 방향을 함께 만들어 주세요',
'shareOpinion.toast.description': '유용한 점, 혼란스러운 점, 부족한 점을 알려 주셔서 다음 방향을 함께 만들어 주세요.',
'sessions.sidebar.footer.actions.shareOpinion': '의견 공유',
'layout.mainTab.chat': '채팅',
'layout.mainTab.plan': '계획',
'layout.mainTab.diff': '변경사항',
+7
View File
@@ -24,6 +24,13 @@ export const dict: Record<I18nKey, string> = {
'pwa.installPrompt.dismiss': 'Odrzuć',
'pwa.installPrompt.started': 'Instalacja rozpoczęta',
'pwa.installPrompt.installed': 'OpenChamber został zainstalowany',
'shareOpinion.actions.shareOpinion': 'Podziel się opinią',
'shareOpinion.actions.bookCall': 'Umów rozmowę',
'shareOpinion.actions.shortSurvey': 'Krótka ankieta',
'shareOpinion.dialog.title': 'Podziel się opinią',
'shareOpinion.toast.title': 'Pomóż kształtować OpenChamber',
'shareOpinion.toast.description': 'Powiedz, co jest przydatne, mylące albo czego brakuje, aby pomóc kształtować kolejne kroki.',
'sessions.sidebar.footer.actions.shareOpinion': 'Podziel się opinią',
'layout.mainTab.chat': 'Czat',
'layout.mainTab.plan': 'Plan',
'layout.mainTab.diff': 'Różnice',
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
"pwa.installPrompt.dismiss": "Dispensar",
"pwa.installPrompt.started": "Instalação iniciada",
"pwa.installPrompt.installed": "OpenChamber instalado",
"shareOpinion.actions.shareOpinion": "Compartilhar opinião",
"shareOpinion.actions.bookCall": "Agendar chamada",
"shareOpinion.actions.shortSurvey": "Pesquisa breve",
"shareOpinion.dialog.title": "Compartilhe sua opinião",
"shareOpinion.toast.title": "Ajude a moldar o OpenChamber",
"shareOpinion.toast.description": "Conte o que é útil, confuso ou está faltando para ajudar a definir os próximos passos.",
"sessions.sidebar.footer.actions.shareOpinion": "Compartilhar opinião",
"layout.mainTab.chat": "Chat",
"layout.mainTab.plan": "Plano",
"layout.mainTab.diff": "Diff",
+7
View File
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
"pwa.installPrompt.dismiss": "Сховати",
"pwa.installPrompt.started": "Установлення розпочато",
"pwa.installPrompt.installed": "OpenChamber встановлено",
"shareOpinion.actions.shareOpinion": "Поділитися думкою",
"shareOpinion.actions.bookCall": "Забронювати дзвінок",
"shareOpinion.actions.shortSurvey": "Коротке опитування",
"shareOpinion.dialog.title": "Поділіться своєю думкою",
"shareOpinion.toast.title": "Допоможіть сформувати OpenChamber",
"shareOpinion.toast.description": "Розкажіть, що корисне, незрозуміле чи відсутнє, щоб допомогти сформувати наступні кроки.",
"sessions.sidebar.footer.actions.shareOpinion": "Поділитися думкою",
"layout.mainTab.chat": "Чат",
"layout.mainTab.plan": "План",
"layout.mainTab.diff": "Diff",
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
'pwa.installPrompt.dismiss': '忽略',
'pwa.installPrompt.started': '已开始安装',
'pwa.installPrompt.installed': 'OpenChamber 已安装',
'shareOpinion.actions.shareOpinion': '分享意见',
'shareOpinion.actions.bookCall': '预约通话',
'shareOpinion.actions.shortSurvey': '简短问卷',
'shareOpinion.dialog.title': '分享你的意见',
'shareOpinion.toast.title': '帮助塑造 OpenChamber',
'shareOpinion.toast.description': '告诉我们哪些有用、令人困惑或缺失,帮助塑造下一步。',
'sessions.sidebar.footer.actions.shareOpinion': '分享意见',
'layout.mainTab.chat': '聊天',
'layout.mainTab.plan': '计划',
'layout.mainTab.diff': '差异',
@@ -23,6 +23,13 @@ export const dict: Record<I18nKey, string> = {
'pwa.installPrompt.dismiss': '忽略',
'pwa.installPrompt.started': '已開始安裝',
'pwa.installPrompt.installed': 'OpenChamber 已安裝',
'shareOpinion.actions.shareOpinion': '分享意見',
'shareOpinion.actions.bookCall': '預約通話',
'shareOpinion.actions.shortSurvey': '簡短問卷',
'shareOpinion.dialog.title': '分享你的意見',
'shareOpinion.toast.title': '協助塑造 OpenChamber',
'shareOpinion.toast.description': '告訴我們哪些有用、令人困惑或缺少什麼,協助塑造下一步。',
'sessions.sidebar.footer.actions.shareOpinion': '分享意見',
'layout.mainTab.chat': '聊天',
'layout.mainTab.plan': '計畫',
'layout.mainTab.diff': '差異',