mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-25 00:45:27 -04:00
[Website] Replace product page hero visual with interactive CRM depicting AI chat in action. (#20566)
Before: <img width="1439" height="518" alt="image" src="https://github.com/user-attachments/assets/4d294a1b-c5a0-43b2-9895-61a8ee19da62" /> After: https://github.com/user-attachments/assets/c019586f-ef9f-4ae0-8afe-14f08e8cb057
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -98,11 +98,6 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
i18n.locale as AppLocale,
|
||||
)}
|
||||
/>
|
||||
<link
|
||||
as="fetch"
|
||||
href="/illustrations/product/hero/hero.glb"
|
||||
rel="preload"
|
||||
/>
|
||||
<Menu.Root
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
@@ -137,7 +132,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.ProductVisual />
|
||||
<Hero.ProductVisual visual={APP_PREVIEW_DATA.visual} />
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root
|
||||
|
||||
@@ -71,6 +71,7 @@ export const SHARED_PEOPLE_AVATAR_URLS = {
|
||||
reidHoffman: '/images/shared/people/avatars/reid-hoffman.webp',
|
||||
roelofBotha: '/images/shared/people/avatars/roelof-botha.webp',
|
||||
ryanRoslansky: '/images/shared/people/avatars/ryan-roslansky.webp',
|
||||
samAltman: '/images/shared/people/avatars/sam-altman.webp',
|
||||
steveAnavi: '/images/shared/people/avatars/steve-anavi.webp',
|
||||
stewartButterfield: '/images/shared/people/avatars/stewart-butterfield.webp',
|
||||
sundarPichai: '/images/shared/people/avatars/sundar-pichai.webp',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ACCENT } from './dashboard-visual.data';
|
||||
import { ACCENT, ACCENT_SECONDARY } from './dashboard-visual.data';
|
||||
import {
|
||||
BG_PANEL,
|
||||
BORDER_COLOR,
|
||||
@@ -16,11 +16,11 @@ const Panel = styled.div`
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding: 18px 20px;
|
||||
padding: 14px 16px;
|
||||
`;
|
||||
|
||||
const PanelTitle = styled.span`
|
||||
@@ -30,13 +30,60 @@ const PanelTitle = styled.span`
|
||||
letter-spacing: 0.02em;
|
||||
`;
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const ChartBody = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const YAxisTitle = styled.span`
|
||||
align-self: center;
|
||||
color: ${TEXT_MUTED};
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.03em;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
`;
|
||||
|
||||
const YAxis = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
const YLabel = styled.span`
|
||||
color: ${TEXT_MUTED};
|
||||
font-size: 9px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
const BarsContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const BarsArea = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
padding-top: 20px;
|
||||
`;
|
||||
|
||||
const BarColumn = styled.div`
|
||||
@@ -44,10 +91,29 @@ const BarColumn = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const BarPair = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const BarWithLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
width: 40%;
|
||||
`;
|
||||
|
||||
const Bar = styled.div`
|
||||
@@ -57,18 +123,15 @@ const Bar = styled.div`
|
||||
transition:
|
||||
height 0.8s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
filter 0.15s ease;
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const BarValue = styled.span`
|
||||
color: ${TEXT_SECONDARY};
|
||||
font-size: 9px;
|
||||
font-size: 8px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 500;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(calc(-100% - 4px));
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const BarLabel = styled.span`
|
||||
@@ -78,9 +141,18 @@ const BarLabel = styled.span`
|
||||
letter-spacing: 0.02em;
|
||||
`;
|
||||
|
||||
const XAxisTitle = styled.div`
|
||||
color: ${TEXT_MUTED};
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.03em;
|
||||
padding-top: 6px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type BarDatum = {
|
||||
label: string;
|
||||
value: number;
|
||||
value2: number;
|
||||
};
|
||||
|
||||
type BarChartProps = {
|
||||
@@ -88,41 +160,85 @@ type BarChartProps = {
|
||||
data: BarDatum[];
|
||||
};
|
||||
|
||||
const Y_TICK_COUNT = 5;
|
||||
|
||||
export function BarChart({ active, data }: BarChartProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const rawMax = Math.max(...data.map((datum) => datum.value));
|
||||
const maxValue = rawMax * 1.3;
|
||||
const allValues = data.flatMap((datum) => [datum.value, datum.value2]);
|
||||
const maxValue = Math.max(...allValues) * 1.05;
|
||||
const tickStep = Math.ceil(maxValue / Y_TICK_COUNT);
|
||||
const yTicks = Array.from(
|
||||
{ length: Y_TICK_COUNT + 1 },
|
||||
(_, i) => i * tickStep,
|
||||
);
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
<PanelTitle>Monthly volume</PanelTitle>
|
||||
<BarsArea>
|
||||
{data.map((datum, index) => {
|
||||
const heightPercent = (datum.value / maxValue) * 100;
|
||||
const isHovered = hoveredIndex === index;
|
||||
<PanelTitle>Widget name</PanelTitle>
|
||||
<ChartWrapper>
|
||||
<ChartBody>
|
||||
<YAxisTitle>Values</YAxisTitle>
|
||||
<YAxis>
|
||||
{[...yTicks].reverse().map((tick) => (
|
||||
<YLabel key={tick}>{tick === 0 ? '0' : `${tick}K`}</YLabel>
|
||||
))}
|
||||
</YAxis>
|
||||
<BarsContainer>
|
||||
<BarsArea>
|
||||
{data.map((datum, index) => {
|
||||
const h1 = (datum.value / maxValue) * 100;
|
||||
const h2 = (datum.value2 / maxValue) * 100;
|
||||
const isHovered = hoveredIndex === index;
|
||||
|
||||
return (
|
||||
<BarColumn key={index}>
|
||||
{active ? (
|
||||
<BarValue style={{ opacity: isHovered ? 1 : 0.7 }}>
|
||||
{datum.value}
|
||||
</BarValue>
|
||||
) : null}
|
||||
<Bar
|
||||
onPointerEnter={() => setHoveredIndex(index)}
|
||||
onPointerLeave={() => setHoveredIndex(null)}
|
||||
style={{
|
||||
backgroundColor: ACCENT,
|
||||
filter: isHovered ? 'brightness(1.3)' : 'none',
|
||||
height: active ? `${heightPercent}%` : '0%',
|
||||
transitionDelay: active ? `${index * 50}ms` : '0ms',
|
||||
}}
|
||||
/>
|
||||
<BarLabel>{datum.label}</BarLabel>
|
||||
</BarColumn>
|
||||
);
|
||||
})}
|
||||
</BarsArea>
|
||||
return (
|
||||
<BarColumn
|
||||
key={index}
|
||||
onPointerEnter={() => setHoveredIndex(index)}
|
||||
onPointerLeave={() => setHoveredIndex(null)}
|
||||
>
|
||||
<BarPair>
|
||||
<BarWithLabel>
|
||||
{active ? (
|
||||
<BarValue style={{ opacity: isHovered ? 1 : 0.7 }}>
|
||||
{datum.value}
|
||||
</BarValue>
|
||||
) : null}
|
||||
<Bar
|
||||
style={{
|
||||
backgroundColor: ACCENT,
|
||||
filter: isHovered ? 'brightness(1.2)' : 'none',
|
||||
height: active ? `${h1}%` : '0%',
|
||||
transitionDelay: active ? `${index * 50}ms` : '0ms',
|
||||
}}
|
||||
/>
|
||||
</BarWithLabel>
|
||||
<BarWithLabel>
|
||||
{active ? (
|
||||
<BarValue style={{ opacity: isHovered ? 1 : 0.7 }}>
|
||||
{datum.value2}
|
||||
</BarValue>
|
||||
) : null}
|
||||
<Bar
|
||||
style={{
|
||||
backgroundColor: ACCENT_SECONDARY,
|
||||
filter: isHovered ? 'brightness(1.2)' : 'none',
|
||||
height: active ? `${h2}%` : '0%',
|
||||
transitionDelay: active
|
||||
? `${index * 50 + 25}ms`
|
||||
: '0ms',
|
||||
}}
|
||||
/>
|
||||
</BarWithLabel>
|
||||
</BarPair>
|
||||
<BarLabel>{datum.label}</BarLabel>
|
||||
</BarColumn>
|
||||
);
|
||||
})}
|
||||
</BarsArea>
|
||||
<XAxisTitle>Months</XAxisTitle>
|
||||
</BarsContainer>
|
||||
</ChartBody>
|
||||
</ChartWrapper>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,109 @@ import { styled } from '@linaria/react';
|
||||
import { BarChart } from './BarChart';
|
||||
import { BAR_DATA, DONUT_VALUE } from './dashboard-visual.data';
|
||||
import { DonutChart } from './DonutChart';
|
||||
import { WindowChrome } from './WindowChrome';
|
||||
import {
|
||||
BG_DARK,
|
||||
BG_PANEL,
|
||||
BORDER_COLOR,
|
||||
TEXT_MUTED,
|
||||
TEXT_PRIMARY,
|
||||
TEXT_SECONDARY,
|
||||
} from './visual-tokens';
|
||||
|
||||
const Window = styled.div`
|
||||
background-color: ${BG_DARK};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Topbar = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${BORDER_COLOR};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
`;
|
||||
|
||||
const BreadcrumbNav = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const BreadcrumbIcon = styled.span`
|
||||
align-items: center;
|
||||
color: ${TEXT_SECONDARY};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const BreadcrumbText = styled.span`
|
||||
color: ${TEXT_SECONDARY};
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.01em;
|
||||
`;
|
||||
|
||||
const BreadcrumbBold = styled.span`
|
||||
color: ${TEXT_PRIMARY};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
`;
|
||||
|
||||
const TopbarActions = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const ActionBtn = styled.span`
|
||||
align-items: center;
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
border-radius: 4px;
|
||||
color: ${TEXT_SECONDARY};
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
`;
|
||||
|
||||
const ActionIcon = styled.span`
|
||||
color: ${TEXT_MUTED};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr 60px;
|
||||
min-height: 0;
|
||||
padding: 14px;
|
||||
`;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
const BarChartCell = styled.div`
|
||||
grid-column: 2;
|
||||
grid-row: 1 / -1;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const BottomPanel = styled.div`
|
||||
background-color: ${BG_PANEL};
|
||||
border: 1px solid ${BORDER_COLOR};
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
`;
|
||||
|
||||
const WidgetTitle = styled.span`
|
||||
color: ${TEXT_SECONDARY};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
`;
|
||||
|
||||
type DashboardVisualProps = {
|
||||
@@ -27,15 +116,61 @@ type DashboardVisualProps = {
|
||||
|
||||
export function DashboardVisual({ active }: DashboardVisualProps) {
|
||||
return (
|
||||
<WindowChrome
|
||||
breadcrumb="Dashboard"
|
||||
breadcrumbBold="Sales performance"
|
||||
title="Apple"
|
||||
>
|
||||
<Window>
|
||||
<Topbar>
|
||||
<BreadcrumbNav>
|
||||
<BreadcrumbIcon>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</BreadcrumbIcon>
|
||||
<BreadcrumbText>Dashboard /</BreadcrumbText>
|
||||
<BreadcrumbBold>Sales performances</BreadcrumbBold>
|
||||
</BreadcrumbNav>
|
||||
<TopbarActions>
|
||||
<ActionBtn>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
|
||||
</svg>
|
||||
Delete
|
||||
</ActionBtn>
|
||||
<ActionIcon>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="5" r="1.5" />
|
||||
<circle cx="12" cy="12" r="1.5" />
|
||||
<circle cx="12" cy="19" r="1.5" />
|
||||
</svg>
|
||||
</ActionIcon>
|
||||
<ActionBtn>⌘K</ActionBtn>
|
||||
</TopbarActions>
|
||||
</Topbar>
|
||||
<Body>
|
||||
<DonutChart active={active} value={DONUT_VALUE} />
|
||||
<BarChart active={active} data={BAR_DATA} />
|
||||
<BarChartCell>
|
||||
<BarChart active={active} data={BAR_DATA} />
|
||||
</BarChartCell>
|
||||
<BottomPanel>
|
||||
<WidgetTitle>Widget name</WidgetTitle>
|
||||
</BottomPanel>
|
||||
</Body>
|
||||
</WindowChrome>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function DonutChart({ active, value }: DonutChartProps) {
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
<PanelTitle>Conversion rate</PanelTitle>
|
||||
<PanelTitle>Widget name</PanelTitle>
|
||||
<ChartArea>
|
||||
<svg height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} width={SIZE}>
|
||||
<circle
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
export const ACCENT = '#16a34a';
|
||||
export const ACCENT_DIM = 'rgba(22, 163, 74, 0.25)';
|
||||
export const ACCENT = '#4A38F5';
|
||||
export const ACCENT_DIM = 'rgba(74, 56, 245, 0.25)';
|
||||
export const ACCENT_SECONDARY = '#7938B2';
|
||||
|
||||
export const BAR_DATA = [
|
||||
{ label: 'Jan', value: 38 },
|
||||
{ label: 'Feb', value: 41 },
|
||||
{ label: 'Mar', value: 40 },
|
||||
{ label: 'Apr', value: 88 },
|
||||
{ label: 'May', value: 44 },
|
||||
{ label: 'Jun', value: 30 },
|
||||
{ label: 'Jul', value: 39 },
|
||||
{ label: 'Aug', value: 44 },
|
||||
{ label: 'Sep', value: 30 },
|
||||
{ label: 'Oct', value: 30 },
|
||||
{ label: 'Nov', value: 41 },
|
||||
{ label: 'Dec', value: 43 },
|
||||
{ label: 'Jan', value: 36, value2: 38 },
|
||||
{ label: 'Feb', value: 68, value2: 41 },
|
||||
{ label: 'Mar', value: 40, value2: 38 },
|
||||
{ label: 'Apr', value: 44, value2: 30 },
|
||||
{ label: 'May', value: 39, value2: 40 },
|
||||
{ label: 'Jun', value: 30, value2: 30 },
|
||||
{ label: 'Jul', value: 44, value2: 30 },
|
||||
];
|
||||
|
||||
export const DONUT_VALUE = 70;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { WebGlMount } from '@/lib/visual-runtime';
|
||||
import { Product } from '@/sections/Hero/visuals/components/Product';
|
||||
import { ProductPlaceholder } from '@/sections/Hero/visuals/components/ProductPlaceholder';
|
||||
|
||||
export function ProductVisual() {
|
||||
return (
|
||||
<WebGlMount priority fallback={<ProductPlaceholder />}>
|
||||
<Product />
|
||||
</WebGlMount>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import type { AppPreviewConfig } from '@/sections/AppPreview';
|
||||
import { COLORS } from '@/sections/AppPreview/Shared/utils/app-preview-theme';
|
||||
import { VISUAL_TOKENS } from '@/sections/AppPreview/Shared/utils/app-preview-tokens';
|
||||
import { AppPreviewNavbar } from '@/sections/AppPreview/Shell/AppPreviewNavbar';
|
||||
import { AppPreviewSidebar } from '@/sections/AppPreview/Shell/AppPreviewSidebar';
|
||||
import { AppPreviewViewbar } from '@/sections/AppPreview/Shell/AppPreviewViewbar';
|
||||
import { renderPageDefinition } from '@/sections/AppPreview/Shell/PageRenderers';
|
||||
import { AppWindow } from '@/sections/AppPreview/AppWindow/AppWindow';
|
||||
import { WindowOrderProvider } from '@/sections/AppPreview/WindowOrder/WindowOrderProvider';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
import { PROMPT_OPTIONS } from './product-visual.data';
|
||||
import { useProductVisualAutoplay } from './use-product-visual-autoplay';
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
isolation: isolate;
|
||||
margin-top: ${theme.spacing(5)};
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
margin-top: ${theme.spacing(8)};
|
||||
}
|
||||
`;
|
||||
|
||||
const ShellScene = styled.div`
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: 0 auto;
|
||||
max-height: 740px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
aspect-ratio: 1280 / 832;
|
||||
}
|
||||
`;
|
||||
|
||||
const AppLayout = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const RightColumn = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 12px 12px 12px 0;
|
||||
`;
|
||||
|
||||
const ContentRow = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const IndexSurface = styled.div`
|
||||
background: ${COLORS.background};
|
||||
border: 1px solid ${COLORS.border};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
[aria-label*='workflow'] > div > div {
|
||||
left: 0;
|
||||
transform: scale(0.65) translateX(-20%);
|
||||
transform-origin: top left;
|
||||
}
|
||||
`;
|
||||
|
||||
const AiPanel = styled.aside`
|
||||
background: ${VISUAL_TOKENS.background.primary};
|
||||
border: 1px solid ${VISUAL_TOKENS.border.color.medium};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
width: 280px;
|
||||
|
||||
@media (max-width: ${theme.breakpoints.md}px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const AiPanelHeader = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${VISUAL_TOKENS.background.secondary};
|
||||
border-bottom: 1px solid ${VISUAL_TOKENS.border.color.medium};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
height: 40px;
|
||||
padding: 0 8px;
|
||||
`;
|
||||
|
||||
const AiHeaderBtn = styled.span`
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: ${VISUAL_TOKENS.font.color.secondary};
|
||||
display: flex;
|
||||
height: 28px;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
`;
|
||||
|
||||
const AiPanelTitle = styled.span`
|
||||
color: ${VISUAL_TOKENS.font.color.primary};
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const AiMessages = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const UserMsg = styled.div`
|
||||
background: ${VISUAL_TOKENS.background.transparent.medium};
|
||||
border-radius: ${VISUAL_TOKENS.border.radius.sm};
|
||||
color: ${VISUAL_TOKENS.font.color.secondary};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4em;
|
||||
padding: 4px 8px;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const AiMsg = styled.div`
|
||||
color: ${VISUAL_TOKENS.font.color.primary};
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4em;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ThinkingText = styled.span`
|
||||
color: ${VISUAL_TOKENS.font.color.tertiary};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const PromptOption = styled.button`
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${VISUAL_TOKENS.font.color.primary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
line-height: 1.4;
|
||||
padding: 6px 4px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${VISUAL_TOKENS.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const PromptOptionIcon = styled.span`
|
||||
align-items: center;
|
||||
color: ${VISUAL_TOKENS.font.color.secondary};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const PromptOptions = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 12px;
|
||||
`;
|
||||
|
||||
const AiInputArea = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const AiInputBox = styled.div`
|
||||
background-color: ${VISUAL_TOKENS.background.transparent.lighter};
|
||||
border: 1px solid ${VISUAL_TOKENS.border.color.medium};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100px;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const AiInputPlaceholder = styled.span`
|
||||
color: ${VISUAL_TOKENS.font.color.light};
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
const AiInputBtnRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
`;
|
||||
|
||||
const AiInputLeftBtns = styled.div`
|
||||
align-items: center;
|
||||
color: ${VISUAL_TOKENS.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
const AiInputRightBtns = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const ModelChip = styled.span`
|
||||
align-items: center;
|
||||
border: 1px solid ${VISUAL_TOKENS.border.color.medium};
|
||||
border-radius: 6px;
|
||||
color: ${VISUAL_TOKENS.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
`;
|
||||
|
||||
const SendBtn = styled.span`
|
||||
align-items: center;
|
||||
background: ${VISUAL_TOKENS.border.color.medium};
|
||||
border-radius: 50%;
|
||||
color: ${VISUAL_TOKENS.font.color.tertiary};
|
||||
display: flex;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
type ProductVisualProps = {
|
||||
visual: AppPreviewConfig;
|
||||
};
|
||||
|
||||
export function ProductVisual({ visual }: ProductVisualProps) {
|
||||
const {
|
||||
activeItem,
|
||||
activeLabel,
|
||||
displayPage,
|
||||
handleOptionSelect,
|
||||
handleSelectLabel,
|
||||
handleToggleFolder,
|
||||
highlightedItemId,
|
||||
openFolderIds,
|
||||
revealedObjectIds,
|
||||
selectedOption,
|
||||
streamComplete,
|
||||
streamedText,
|
||||
workspaceNav,
|
||||
} = useProductVisualAutoplay(visual);
|
||||
|
||||
const activeHeader = displayPage?.header;
|
||||
const showViewBar =
|
||||
displayPage !== null &&
|
||||
displayPage !== undefined &&
|
||||
displayPage.type !== 'dashboard' &&
|
||||
displayPage.type !== 'workflow';
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<ShellScene>
|
||||
<WindowOrderProvider>
|
||||
<AppWindow>
|
||||
<AppLayout>
|
||||
<AppPreviewSidebar
|
||||
favoritesNav={visual.favoritesNav}
|
||||
highlightedItemId={highlightedItemId ?? undefined}
|
||||
onSelectLabel={handleSelectLabel}
|
||||
onToggleFolder={handleToggleFolder}
|
||||
openFolderIds={openFolderIds}
|
||||
selectedLabel={activeLabel}
|
||||
workspaceName={visual.workspace.name}
|
||||
workspaceNav={workspaceNav}
|
||||
/>
|
||||
|
||||
<RightColumn>
|
||||
<AppPreviewNavbar
|
||||
activeItem={activeItem}
|
||||
activeLabel={activeLabel}
|
||||
navbarActions={activeHeader?.navbarActions}
|
||||
revealedObjectIds={revealedObjectIds}
|
||||
/>
|
||||
|
||||
<ContentRow>
|
||||
<IndexSurface>
|
||||
{showViewBar ? (
|
||||
<AppPreviewViewbar
|
||||
actions={activeHeader?.actions ?? []}
|
||||
count={activeHeader?.count}
|
||||
pageType={displayPage.type}
|
||||
showListIcon={activeHeader?.showListIcon ?? false}
|
||||
title={activeHeader?.title ?? activeLabel}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{displayPage
|
||||
? renderPageDefinition(
|
||||
displayPage,
|
||||
handleSelectLabel,
|
||||
activeItem?.id ?? activeLabel,
|
||||
)
|
||||
: null}
|
||||
</IndexSurface>
|
||||
|
||||
<AiPanel>
|
||||
<AiPanelHeader>
|
||||
<AiHeaderBtn>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</AiHeaderBtn>
|
||||
<AiPanelTitle>Ask AI</AiPanelTitle>
|
||||
<AiHeaderBtn>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</AiHeaderBtn>
|
||||
</AiPanelHeader>
|
||||
<AiMessages>
|
||||
<UserMsg>{PROMPT_OPTIONS[selectedOption].label}</UserMsg>
|
||||
{streamedText ? (
|
||||
<AiMsg>{streamedText}</AiMsg>
|
||||
) : (
|
||||
<ThinkingText>Thinking...</ThinkingText>
|
||||
)}
|
||||
</AiMessages>
|
||||
{streamComplete ? (
|
||||
<PromptOptions>
|
||||
{PROMPT_OPTIONS.filter(
|
||||
(_, index) => index !== selectedOption,
|
||||
).map((option, index) => (
|
||||
<PromptOption
|
||||
key={index}
|
||||
onClick={() =>
|
||||
handleOptionSelect(PROMPT_OPTIONS.indexOf(option))
|
||||
}
|
||||
>
|
||||
<PromptOptionIcon>{option.icon}</PromptOptionIcon>
|
||||
{option.label}
|
||||
</PromptOption>
|
||||
))}
|
||||
</PromptOptions>
|
||||
) : null}
|
||||
<AiInputArea>
|
||||
<AiInputBox>
|
||||
<AiInputPlaceholder>
|
||||
Ask, search or make anything...
|
||||
</AiInputPlaceholder>
|
||||
<AiInputBtnRow>
|
||||
<AiInputLeftBtns>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
|
||||
</svg>
|
||||
</AiInputLeftBtns>
|
||||
<AiInputRightBtns>
|
||||
<ModelChip>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#D97757"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
Claude Haiku 4.5
|
||||
<svg
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</ModelChip>
|
||||
<SendBtn>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="19" x2="12" y2="5" />
|
||||
<polyline points="5 12 12 5 19 12" />
|
||||
</svg>
|
||||
</SendBtn>
|
||||
</AiInputRightBtns>
|
||||
</AiInputBtnRow>
|
||||
</AiInputBox>
|
||||
</AiInputArea>
|
||||
</AiPanel>
|
||||
</ContentRow>
|
||||
</RightColumn>
|
||||
</AppLayout>
|
||||
</AppWindow>
|
||||
</WindowOrderProvider>
|
||||
</ShellScene>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { SHARED_PEOPLE_AVATAR_URLS } from '@/content/site/asset-paths';
|
||||
|
||||
export const NEW_COMPANY_ROW = {
|
||||
id: 'openai',
|
||||
cells: {
|
||||
company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' },
|
||||
url: { type: 'link' as const, value: 'openai.com' },
|
||||
createdBy: {
|
||||
type: 'person' as const,
|
||||
name: 'AI Agent',
|
||||
tone: 'gray' as const,
|
||||
kind: 'system' as const,
|
||||
shortLabel: 'AI',
|
||||
},
|
||||
address: { type: 'text' as const, value: '3180 18th St' },
|
||||
accountOwner: {
|
||||
type: 'person' as const,
|
||||
name: 'Sam Altman',
|
||||
tone: 'amber' as const,
|
||||
kind: 'person' as const,
|
||||
avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman,
|
||||
},
|
||||
icp: { type: 'boolean' as const, value: true },
|
||||
arr: { type: 'number' as const, value: '$2,000,000' },
|
||||
linkedin: { type: 'link' as const, value: 'openai' },
|
||||
industry: { type: 'tag' as const, value: 'AI Research' },
|
||||
mainContact: {
|
||||
type: 'person' as const,
|
||||
name: 'Sam Altman',
|
||||
shortLabel: 'S',
|
||||
tone: 'amber' as const,
|
||||
kind: 'person' as const,
|
||||
avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman,
|
||||
},
|
||||
employees: { type: 'number' as const, value: '3,500' },
|
||||
opportunities: { type: 'relation' as const, items: [] },
|
||||
added: { type: 'text' as const, value: 'Just now' },
|
||||
},
|
||||
};
|
||||
|
||||
export const NEW_PERSON_ROW = {
|
||||
id: 'sam-altman',
|
||||
cells: {
|
||||
name: {
|
||||
type: 'person' as const,
|
||||
name: 'Sam Altman',
|
||||
tone: 'amber' as const,
|
||||
kind: 'person' as const,
|
||||
avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman,
|
||||
},
|
||||
company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' },
|
||||
email: { type: 'link' as const, value: 'sam@openai.com' },
|
||||
phone: { type: 'text' as const, value: '+1 415 555 0199' },
|
||||
jobTitle: { type: 'text' as const, value: 'CEO' },
|
||||
city: { type: 'text' as const, value: 'San Francisco' },
|
||||
linkedin: { type: 'link' as const, value: 'sama' },
|
||||
added: { type: 'text' as const, value: 'Just now' },
|
||||
},
|
||||
};
|
||||
|
||||
export const PROMPT_OPTIONS = [
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Add a new lead',
|
||||
navSteps: [
|
||||
{ at: 0.25, target: 'Companies' },
|
||||
{ at: 0.65, target: 'People' },
|
||||
],
|
||||
response:
|
||||
'Adding OpenAI as a new company. Setting domain to openai.com, industry to AI Research, and ARR to $2,000,000. Account owner assigned to Sam Altman. Company record is live in your CRM.\n\nNow creating the contact — adding Sam Altman as CEO at OpenAI, based in San Francisco. Person record linked to the company.',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Create a dashboard',
|
||||
navSteps: [
|
||||
{ at: 0.35, target: 'Dashboards' },
|
||||
{ at: 0.75, target: 'Sales Dashboard' },
|
||||
],
|
||||
response:
|
||||
"Building a Sales Performance dashboard. I'll include revenue by month, new subscriptions, churn rate, and a distribution chart. Navigating to dashboards now... Opening the Sales Dashboard — your metrics are ready.",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15 1.65 1.65 0 003 14.08V14a2 2 0 014 0v.09" />
|
||||
</svg>
|
||||
),
|
||||
label: 'Set up a workflow',
|
||||
navSteps: [{ at: 0.45, target: 'Create company when adding a new person' }],
|
||||
response:
|
||||
'Creating an automation: when a new person is added, automatically create their company record and link them. Setting the trigger and actions now... Workflow is active and ready to run.',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AppPreviewConfig } from '@/sections/AppPreview';
|
||||
import { useAppPreviewState } from '@/sections/AppPreview/Shell/use-app-preview-state';
|
||||
|
||||
import {
|
||||
NEW_COMPANY_ROW,
|
||||
NEW_PERSON_ROW,
|
||||
PROMPT_OPTIONS,
|
||||
} from './product-visual.data';
|
||||
|
||||
export function useProductVisualAutoplay(visual: AppPreviewConfig) {
|
||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||
const [streamedText, setStreamedText] = useState('');
|
||||
const [streamComplete, setStreamComplete] = useState(false);
|
||||
const [companyAdded, setCompanyAdded] = useState(false);
|
||||
const [personAdded, setPersonAdded] = useState(false);
|
||||
|
||||
const {
|
||||
activeItem,
|
||||
activeLabel,
|
||||
activePage,
|
||||
handleSelectLabel,
|
||||
handleToggleFolder,
|
||||
highlightedItemId,
|
||||
openFolderIds,
|
||||
revealedObjectIds,
|
||||
workspaceNav,
|
||||
} = useAppPreviewState(visual);
|
||||
|
||||
let displayPage = activePage;
|
||||
if (
|
||||
activePage !== null &&
|
||||
activePage !== undefined &&
|
||||
activePage.type === 'table'
|
||||
) {
|
||||
const title = activePage.header?.title;
|
||||
if (companyAdded && title === 'All Companies') {
|
||||
displayPage = {
|
||||
...activePage,
|
||||
header: {
|
||||
...activePage.header,
|
||||
count: (activePage.header.count ?? 0) + 1,
|
||||
},
|
||||
rows: [NEW_COMPANY_ROW, ...activePage.rows],
|
||||
};
|
||||
} else if (personAdded && title === 'All People') {
|
||||
displayPage = {
|
||||
...activePage,
|
||||
header: {
|
||||
...activePage.header,
|
||||
count: (activePage.header.count ?? 0) + 1,
|
||||
},
|
||||
rows: [NEW_PERSON_ROW, ...activePage.rows],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const option = PROMPT_OPTIONS[selectedOption];
|
||||
const fullText = option.response;
|
||||
let index = 0;
|
||||
const completedSteps = new Set<number>();
|
||||
let companyInjected = false;
|
||||
let personInjected = false;
|
||||
setStreamedText('');
|
||||
setStreamComplete(false);
|
||||
setCompanyAdded(false);
|
||||
setPersonAdded(false);
|
||||
const interval = setInterval(() => {
|
||||
index += 1;
|
||||
setStreamedText(fullText.slice(0, index));
|
||||
const progress = index / fullText.length;
|
||||
option.navSteps.forEach((step, stepIndex) => {
|
||||
if (!completedSteps.has(stepIndex) && progress >= step.at) {
|
||||
completedSteps.add(stepIndex);
|
||||
handleSelectLabel(step.target);
|
||||
}
|
||||
});
|
||||
if (selectedOption === 0) {
|
||||
if (!companyInjected && progress >= 0.2) {
|
||||
companyInjected = true;
|
||||
setCompanyAdded(true);
|
||||
}
|
||||
if (!personInjected && progress >= 0.6) {
|
||||
personInjected = true;
|
||||
setPersonAdded(true);
|
||||
}
|
||||
}
|
||||
if (index >= fullText.length) {
|
||||
clearInterval(interval);
|
||||
setStreamComplete(true);
|
||||
}
|
||||
}, 20);
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedOption, handleSelectLabel]);
|
||||
|
||||
const handleOptionSelect = useCallback(
|
||||
(optionIndex: number) => setSelectedOption(optionIndex),
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
activeItem,
|
||||
activeLabel,
|
||||
displayPage,
|
||||
handleOptionSelect,
|
||||
handleSelectLabel,
|
||||
handleToggleFolder,
|
||||
highlightedItemId,
|
||||
openFolderIds,
|
||||
revealedObjectIds,
|
||||
selectedOption,
|
||||
streamComplete,
|
||||
streamedText,
|
||||
workspaceNav,
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Cta } from './Cta';
|
||||
import { Heading } from './Heading';
|
||||
import { AppPreview } from '@/sections/AppPreview';
|
||||
import { PartnerVisual } from './PartnerVisual/PartnerVisual';
|
||||
import { ProductVisual } from './ProductVisual';
|
||||
import { ProductVisual } from './ProductVisual/ProductVisual';
|
||||
import { ReleaseNotesVisual } from './ReleaseNotesVisual';
|
||||
import { Root } from './Root';
|
||||
import { WhyTwentyVisual } from './WhyTwentyVisual';
|
||||
|
||||
Reference in New Issue
Block a user