This commit is contained in:
Flatlogic Bot 2026-04-05 17:48:33 +00:00
parent 2d686911e7
commit d3ec77b828
6 changed files with 1196 additions and 148 deletions

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -0,0 +1,179 @@
export type HubLinkType = 'agent' | 'demo' | 'form' | 'social' | 'media';
export type HubLink = {
id: string;
title: string;
url: string;
type: HubLinkType;
summary: string;
eyebrow: string;
accent: string;
};
export type MediaPresetType = 'radio' | 'tv';
export type MediaPresetMode = 'audio' | 'video' | 'embed';
export type MediaPreset = {
id: string;
type: MediaPresetType;
title: string;
url: string;
notes: string;
mode: MediaPresetMode;
isSample?: boolean;
};
export type AdminShortcut = {
title: string;
href: string;
summary: string;
};
export const modularInteractionLinks: HubLink[] = [
{
id: 'agent-prof-aliyo',
title: 'Prof Aliyo Momot AI Agent',
url: 'https://prof-aliyo-momot-ai-agent.onhercules.app/agent',
type: 'agent',
eyebrow: 'AI agent',
summary: 'Primary conversational entry point for visitors who want guided AI interaction.',
accent: 'from-fuchsia-500/20 via-violet-500/10 to-cyan-400/20',
},
{
id: 'agent-frame-smith',
title: 'Frame Smith AI',
url: 'https://frame-smith-ai.lovable.app',
type: 'agent',
eyebrow: 'Creative workflow',
summary: 'A second AI experience positioned for making, framing, and ideation workflows.',
accent: 'from-sky-500/20 via-cyan-500/10 to-emerald-400/20',
},
{
id: 'demo-aliyo-media',
title: 'Aliyo Momot Media Demo',
url: 'https://aliyo-momot-media-fb6f.dev.flatlogic.app/?stream=d5ade7f8-7b12-4b2b-997e-4f1de9180e9b',
type: 'media',
eyebrow: 'Media experience',
summary: 'Existing demo destination for media-first storytelling and streaming discovery.',
accent: 'from-amber-500/20 via-orange-500/10 to-rose-400/20',
},
{
id: 'demo-copyright-revealer',
title: 'Copyright Revealer',
url: 'https://copyright-revealer-cc67.dev.flatlogic.app',
type: 'demo',
eyebrow: 'Specialized demo',
summary: 'A supporting utility demo that expands the modular toolset around your concept.',
accent: 'from-indigo-500/20 via-blue-500/10 to-cyan-400/20',
},
{
id: 'form-1',
title: 'Fillout Intake 01',
url: 'https://build.fillout.com/use/fwxccezugw',
type: 'form',
eyebrow: 'Fillout form',
summary: 'Capture structured visitor input without forcing them to leave your ecosystem.',
accent: 'from-lime-500/20 via-emerald-500/10 to-cyan-400/20',
},
{
id: 'form-2',
title: 'Fillout Intake 02',
url: 'https://build.fillout.com/use/fouwuywqjg',
type: 'form',
eyebrow: 'Fillout form',
summary: 'Useful for onboarding, submissions, or modular requests from the public.',
accent: 'from-purple-500/20 via-pink-500/10 to-rose-400/20',
},
{
id: 'form-3',
title: 'Fillout Intake 03',
url: 'https://build.fillout.com/use/1e7hvg7j95',
type: 'form',
eyebrow: 'Fillout form',
summary: 'A third intake surface that keeps lead capture visible inside the link dashboard.',
accent: 'from-cyan-500/20 via-blue-500/10 to-violet-400/20',
},
{
id: 'form-4',
title: 'Fillout Intake 04',
url: 'https://build.fillout.com/use/j1yzoiedhq',
type: 'form',
eyebrow: 'Fillout form',
summary: 'Additional form endpoint for follow-ups, media submissions, or campaign flows.',
accent: 'from-pink-500/20 via-rose-500/10 to-orange-400/20',
},
{
id: 'social-facebook',
title: 'Facebook Community Profile',
url: 'https://www.facebook.com/profile.php?id=61567817647812',
type: 'social',
eyebrow: 'Social presence',
summary: 'A community touchpoint for followers arriving from Facebook and public promotion.',
accent: 'from-blue-500/20 via-indigo-500/10 to-violet-400/20',
},
];
export const starterMediaPresets: MediaPreset[] = [
{
id: 'sample-radio',
type: 'radio',
title: 'Sample Radio Placeholder',
url: 'https://samplelib.com/lib/preview/mp3/sample-12s.mp3',
notes: 'Replace this with your live radio stream URL when you are ready.',
mode: 'audio',
isSample: true,
},
{
id: 'sample-tv',
type: 'tv',
title: 'Sample TV Placeholder',
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
notes: 'This placeholder shows the TV widget layout until you paste a real embed or video stream.',
mode: 'video',
isSample: true,
},
];
export const adminShortcuts: AdminShortcut[] = [
{
title: 'Admin interface',
href: '/login',
summary: 'Sign in to manage all generated CRUD screens and protected content modules.',
},
{
title: 'External links manager',
href: '/external_links/external_links-list',
summary: 'Use the existing CRUD to curate agents, demos, forms, and social links.',
},
{
title: 'Media streams manager',
href: '/media_streams/media_streams-list',
summary: 'Manage stream records for radio and television sources inside the admin area.',
},
{
title: 'Widgets manager',
href: '/widgets/widgets-list',
summary: 'Configure reusable radio, TV, AI, and embed widgets for future authenticated workflows.',
},
];
export const hubHighlights = [
{
title: 'Modular by design',
description: 'Visitors can move between AI, media, and forms without losing the overall story.',
},
{
title: 'Public-friendly',
description: 'The front door is open, branded, and focused on exploration instead of admin jargon.',
},
{
title: 'Admin-ready',
description: 'Existing CRUD screens stay in place so you can manage content once you log in.',
},
];
export const experienceSteps = [
'Open the public landing page and understand the Modular Artificial Interaction concept at a glance.',
'Jump into the interaction hub to browse AI agents, demos, forms, and community links.',
'Preview radio or TV in-page, or paste your own stream URL and save it as a preset in this browser.',
];

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

View File

@ -7,6 +7,13 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
{
href: '/interaction-hub',
label: 'Interaction hub',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiAtomVariant' in icon ? icon['mdiAtomVariant' as keyof typeof icon] : icon.mdiBroadcast ?? icon.mdiViewDashboardOutline,
},
{
href: '/users/users-list',

View File

@ -1,166 +1,348 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import {
mdiArrowRight,
mdiBroadcast,
mdiCubeOutline,
mdiLogin,
mdiOpenInNew,
mdiPlayCircleOutline,
mdiRobotOutline,
mdiTelevisionPlay,
mdiViewDashboardOutline,
} from '@mdi/js';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import BaseIcon from '../components/BaseIcon';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import {
adminShortcuts,
experienceSteps,
hubHighlights,
modularInteractionLinks,
} from '../helpers/modularInteractionHub';
const moduleCards = [
{
title: 'AI entry points',
description: 'Launch your existing agents from one branded, public-facing destination.',
icon: mdiRobotOutline,
},
{
title: 'Link switchboard',
description: 'Show demos, Fillout flows, and social destinations without the page feeling scattered.',
icon: mdiCubeOutline,
},
{
title: 'Radio + TV widgets',
description: 'Give visitors in-page media playback with presets and future room for live streams.',
icon: mdiBroadcast,
},
{
title: 'Admin-ready modules',
description: 'Keep the generated admin, CRUD, and permissions setup intact for future growth.',
icon: mdiViewDashboardOutline,
},
];
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const featuredLinks = modularInteractionLinks.slice(0, 6);
const title = 'Modular Interaction Hub'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
const statCards = [
{ value: '2', label: 'AI experiences' },
{ value: '4', label: 'Fillout entry forms' },
{ value: '2', label: 'Media widgets' },
];
export default function HomePage() {
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<>
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Modular Artificial Interaction')}</title>
<meta
name="description"
content="A public-facing AI and media hub for Modular Artificial Interaction, with agent links, forms, and radio/TV widgets."
/>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Modular Interaction Hub app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<div className="min-h-screen bg-[#050816] text-white">
<div className="absolute inset-x-0 top-0 -z-0 h-[38rem] bg-[radial-gradient(circle_at_top,_rgba(96,165,250,0.28),_transparent_42%),radial-gradient(circle_at_30%_30%,_rgba(217,70,239,0.24),_transparent_32%),linear-gradient(180deg,_#0a1024,_#050816)]" />
<header className="sticky top-0 z-20 border-b border-white/10 bg-[#050816]/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
<Link href="/" className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-cyan-400/40 bg-cyan-400/10 text-cyan-200 shadow-[0_0_40px_rgba(34,211,238,0.15)]">
<BaseIcon path={mdiCubeOutline} size={22} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.32em] text-cyan-200/80">Modular Artificial</p>
<p className="text-lg font-semibold tracking-wide text-white">Interaction Hub</p>
</div>
</Link>
<nav className="hidden items-center gap-8 text-sm text-slate-300 md:flex">
<a href="#concept" className="transition hover:text-white">
Concept
</a>
<a href="#modules" className="transition hover:text-white">
Modules
</a>
<a href="#switchboard" className="transition hover:text-white">
Switchboard
</a>
<a href="#admin" className="transition hover:text-white">
Admin
</a>
</nav>
<div className="flex items-center gap-3">
<BaseButton href="/login" label="Login" color="whiteDark" className="border-white/10" />
<BaseButton href="/interaction-hub" label="Open hub" color="info" />
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</div>
</header>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<main className="relative z-10">
<section className="mx-auto grid max-w-7xl gap-12 px-6 pb-16 pt-16 lg:grid-cols-[1.15fr_0.85fr] lg:pb-24 lg:pt-24">
<div className="space-y-8">
<div className="inline-flex items-center gap-2 rounded-full border border-fuchsia-400/25 bg-white/5 px-4 py-2 text-sm text-fuchsia-100 shadow-[0_0_30px_rgba(217,70,239,0.12)]">
<span className="h-2 w-2 rounded-full bg-emerald-400" />
Beautiful public front door for AI, links, and media
</div>
</div>
<div className="space-y-6">
<h1 className="max-w-4xl text-5xl font-semibold leading-tight text-white md:text-6xl">
Present <span className="text-cyan-300">Modular Artificial Interaction</span> as one living experience.
</h1>
<p className="max-w-3xl text-lg leading-8 text-slate-300 md:text-xl">
This first slice turns the starter app into a polished public hub: your AI agents,
demos, Fillout links, and radio/TV widgets now feel connected, intentional, and ready
for visitors coming from social or direct links.
</p>
</div>
<div className="flex flex-wrap gap-4">
<BaseButton href="/interaction-hub" label="Launch interaction hub" icon={mdiArrowRight} color="info" />
<BaseButton href="/login" label="Admin interface" icon={mdiLogin} color="whiteDark" className="border-white/10" />
</div>
<div className="grid gap-4 sm:grid-cols-3">
{statCards.map((item) => (
<div
key={item.label}
className="rounded-3xl border border-white/10 bg-white/5 p-5 backdrop-blur-xl"
>
<div className="text-3xl font-semibold text-white">{item.value}</div>
<div className="mt-2 text-sm uppercase tracking-[0.22em] text-slate-400">
{item.label}
</div>
</div>
))}
</div>
</div>
<div className="relative">
<div className="absolute inset-0 rounded-[2rem] bg-gradient-to-br from-cyan-500/20 via-fuchsia-500/10 to-transparent blur-2xl" />
<div className="relative overflow-hidden rounded-[2rem] border border-white/10 bg-slate-950/75 p-6 shadow-[0_30px_80px_rgba(15,23,42,0.6)] backdrop-blur-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-5">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-cyan-200/70">Live concept board</p>
<h2 className="mt-2 text-2xl font-semibold text-white">How the experience feels</h2>
</div>
<div className="rounded-full border border-emerald-400/40 bg-emerald-400/10 px-3 py-1 text-xs uppercase tracking-[0.2em] text-emerald-200">
Public ready
</div>
</div>
<div className="mt-6 space-y-4">
{hubHighlights.map((item, index) => (
<div
key={item.title}
className="rounded-3xl border border-white/10 bg-white/5 p-5 text-left"
>
<div className="mb-3 flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-white/10 text-cyan-200">
<span className="text-sm font-semibold">0{index + 1}</span>
</div>
<h3 className="text-lg font-medium text-white">{item.title}</h3>
</div>
<p className="text-sm leading-7 text-slate-300">{item.description}</p>
</div>
))}
</div>
</div>
</div>
</section>
<section id="concept" className="mx-auto max-w-7xl px-6 py-6">
<div className="rounded-[2rem] border border-white/10 bg-white/[0.03] p-8 shadow-[0_20px_60px_rgba(15,23,42,0.4)]">
<div className="grid gap-8 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
<div>
<p className="text-sm uppercase tracking-[0.3em] text-cyan-200/70">Concept</p>
<h2 className="mt-4 text-3xl font-semibold text-white md:text-4xl">
A modular stack for interaction, intake, and media.
</h2>
<p className="mt-4 max-w-2xl text-base leading-8 text-slate-300">
Instead of sending people to scattered tools, the app now frames everything as a
coherent digital venue: AI assistance, form-based capture, media playback, and
curated launch points are all organized under one calm, futuristic visual system.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{experienceSteps.map((step, index) => (
<div key={step} className="rounded-3xl border border-white/10 bg-slate-900/60 p-5">
<div className="mb-4 text-sm uppercase tracking-[0.24em] text-fuchsia-200/80">
Step {index + 1}
</div>
<p className="text-sm leading-7 text-slate-300">{step}</p>
</div>
))}
</div>
</div>
</div>
</section>
<section id="modules" className="mx-auto max-w-7xl px-6 py-16">
<div className="mb-10 max-w-3xl">
<p className="text-sm uppercase tracking-[0.3em] text-cyan-200/70">Modules</p>
<h2 className="mt-4 text-3xl font-semibold text-white md:text-4xl">
The first branded slice is more than a landing page.
</h2>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
{moduleCards.map((item) => (
<div
key={item.title}
className="rounded-[1.75rem] border border-white/10 bg-white/[0.04] p-6 transition duration-200 hover:-translate-y-1 hover:border-cyan-300/30 hover:bg-white/[0.06]"
>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-400/10 text-cyan-200">
<BaseIcon path={item.icon} size={22} />
</div>
<h3 className="mt-6 text-xl font-medium text-white">{item.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.description}</p>
</div>
))}
</div>
</section>
<section id="switchboard" className="mx-auto max-w-7xl px-6 py-2">
<div className="mb-10 flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
<div className="max-w-3xl">
<p className="text-sm uppercase tracking-[0.3em] text-cyan-200/70">Switchboard preview</p>
<h2 className="mt-4 text-3xl font-semibold text-white md:text-4xl">
Curated launch cards for the tools you already have.
</h2>
</div>
<BaseButton href="/interaction-hub" label="See the full hub" icon={mdiArrowRight} color="whiteDark" className="border-white/10" />
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{featuredLinks.map((item) => (
<a
key={item.id}
href={item.url}
target="_blank"
rel="noreferrer"
className={`group overflow-hidden rounded-[1.75rem] border border-white/10 bg-gradient-to-br ${item.accent} p-[1px] transition hover:-translate-y-1 hover:border-white/20`}
>
<div className="flex h-full flex-col rounded-[1.7rem] bg-slate-950/90 p-6">
<div className="flex items-center justify-between gap-4">
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.22em] text-slate-300">
{item.eyebrow}
</span>
<BaseIcon path={mdiOpenInNew} size={18} className="text-slate-400 transition group-hover:text-white" />
</div>
<h3 className="mt-6 text-xl font-medium text-white">{item.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.summary}</p>
</div>
</a>
))}
</div>
</section>
<section className="mx-auto max-w-7xl px-6 py-16">
<div className="grid gap-6 lg:grid-cols-2">
<div className="rounded-[2rem] border border-white/10 bg-gradient-to-br from-cyan-500/10 via-slate-950 to-slate-950 p-8">
<div className="flex items-center gap-3 text-cyan-200">
<BaseIcon path={mdiPlayCircleOutline} size={24} />
<p className="text-sm uppercase tracking-[0.25em]">Radio widget</p>
</div>
<h3 className="mt-5 text-2xl font-semibold text-white">In-page listening for your audience</h3>
<p className="mt-4 max-w-xl text-sm leading-7 text-slate-300">
The hub includes a radio player area with browser-saved presets, so you can start with
placeholders now and swap in real stream URLs later.
</p>
</div>
<div className="rounded-[2rem] border border-white/10 bg-gradient-to-br from-fuchsia-500/10 via-slate-950 to-slate-950 p-8">
<div className="flex items-center gap-3 text-fuchsia-200">
<BaseIcon path={mdiTelevisionPlay} size={24} />
<p className="text-sm uppercase tracking-[0.25em]">TV widget</p>
</div>
<h3 className="mt-5 text-2xl font-semibold text-white">Flexible embed or video preview</h3>
<p className="mt-4 max-w-xl text-sm leading-7 text-slate-300">
Visitors can stay on your site while watching a sample video or a future live embed,
giving the experience a richer broadcast identity.
</p>
</div>
</div>
</section>
<section id="admin" className="mx-auto max-w-7xl px-6 pb-20 pt-4">
<div className="rounded-[2rem] border border-white/10 bg-white/[0.04] p-8">
<div className="mb-8 max-w-3xl">
<p className="text-sm uppercase tracking-[0.3em] text-cyan-200/70">Admin continuity</p>
<h2 className="mt-4 text-3xl font-semibold text-white md:text-4xl">
Keep the admin power you already have.
</h2>
<p className="mt-4 text-sm leading-7 text-slate-300">
The new public experience works as the front door, while the generated admin remains the
control room for managing links, streams, widgets, and future content.
</p>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
{adminShortcuts.map((item) => (
<Link
key={item.title}
href={item.href}
className="rounded-[1.5rem] border border-white/10 bg-slate-950/70 p-6 transition hover:-translate-y-1 hover:border-cyan-300/30"
>
<h3 className="text-lg font-medium text-white">{item.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.summary}</p>
</Link>
))}
</div>
</div>
</section>
</main>
<footer className="border-t border-white/10 bg-slate-950/80">
<div className="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-8 text-sm text-slate-400 md:flex-row md:items-center md:justify-between">
<p>© 2026 Modular Artificial Interaction Hub. Built as a public AI + media launch surface.</p>
<div className="flex flex-wrap items-center gap-5">
<Link href="/interaction-hub" className="transition hover:text-white">
Open hub
</Link>
<Link href="/login" className="transition hover:text-white">
Login
</Link>
<Link href="/privacy-policy" className="transition hover:text-white">
Privacy policy
</Link>
<Link href="/terms-of-use" className="transition hover:text-white">
Terms
</Link>
</div>
</div>
</footer>
</div>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
HomePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,682 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import {
mdiArrowLeft,
mdiBroadcast,
mdiDeleteOutline,
mdiOpenInNew,
mdiPlus,
mdiRadioTower,
mdiRobotOutline,
mdiTelevisionPlay,
mdiViewDashboardOutline,
} from '@mdi/js';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
import {
adminShortcuts,
MediaPreset,
MediaPresetMode,
MediaPresetType,
modularInteractionLinks,
starterMediaPresets,
} from '../helpers/modularInteractionHub';
const MEDIA_PRESETS_STORAGE_KEY = 'modular-interaction-media-presets';
type FormState = {
type: MediaPresetType;
title: string;
url: string;
notes: string;
mode: MediaPresetMode;
};
type FormErrors = Partial<Record<keyof FormState, string>>;
const defaultFormState: FormState = {
type: 'radio',
title: '',
url: '',
notes: '',
mode: 'audio',
};
const sectionMeta: Record<string, { title: string; subtitle: string; icon: string }> = {
agent: {
title: 'AI agents',
subtitle: 'Conversation and guidance entry points',
icon: mdiRobotOutline,
},
media: {
title: 'Media demos',
subtitle: 'Existing media-led experiences',
icon: mdiBroadcast,
},
demo: {
title: 'Supporting demos',
subtitle: 'Specialized utility and proof-of-concept pages',
icon: mdiViewDashboardOutline,
},
form: {
title: 'Forms & intake',
subtitle: 'Submission endpoints for structured visitor data',
icon: mdiPlus,
},
social: {
title: 'Community',
subtitle: 'Social touchpoints and audience handoff',
icon: mdiOpenInNew,
},
};
const createId = () => `preset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const isValidUrl = (value: string) => {
try {
const parsed = new URL(value);
return parsed.protocol === 'https:' || parsed.protocol === 'http:';
} catch {
return false;
}
};
const inferMode = (type: MediaPresetType, url: string, fallback: MediaPresetMode) => {
if (type === 'radio') {
return 'audio';
}
const lowered = url.toLowerCase();
if (
lowered.endsWith('.mp4') ||
lowered.endsWith('.webm') ||
lowered.endsWith('.ogg') ||
lowered.includes('.mp4?')
) {
return 'video';
}
if (lowered.includes('youtube.com/watch?v=')) {
return 'embed';
}
return fallback;
};
const normalizeTvUrl = (url: string, mode: MediaPresetMode) => {
if (mode !== 'embed') {
return url;
}
if (url.includes('youtube.com/watch?v=')) {
const parsed = new URL(url);
const videoId = parsed.searchParams.get('v');
if (videoId) {
return `https://www.youtube.com/embed/${videoId}`;
}
}
return url;
};
const getPreviewLabel = (preset: MediaPreset) => {
if (preset.type === 'radio') {
return 'Radio preview';
}
return preset.mode === 'embed' ? 'TV embed preview' : 'TV video preview';
};
export default function InteractionHubPage() {
const [presets, setPresets] = useState<MediaPreset[]>(starterMediaPresets);
const [selectedPresetId, setSelectedPresetId] = useState<string>(starterMediaPresets[0].id);
const [formState, setFormState] = useState<FormState>(defaultFormState);
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [feedbackMessage, setFeedbackMessage] = useState('');
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const storedPresets = window.localStorage.getItem(MEDIA_PRESETS_STORAGE_KEY);
if (!storedPresets) {
return;
}
try {
const parsedPresets = JSON.parse(storedPresets) as MediaPreset[];
if (Array.isArray(parsedPresets) && parsedPresets.length > 0) {
setPresets(parsedPresets);
setSelectedPresetId(parsedPresets[0].id);
}
} catch (error) {
console.error('Failed to load media presets from localStorage:', error);
}
}, []);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(MEDIA_PRESETS_STORAGE_KEY, JSON.stringify(presets));
}, [presets]);
const groupedLinks = useMemo(() => {
return modularInteractionLinks.reduce<Record<string, typeof modularInteractionLinks>>((acc, item) => {
const key = item.type;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {});
}, []);
const selectedPreset = presets.find((preset) => preset.id === selectedPresetId) ?? presets[0];
const radioPresets = presets.filter((preset) => preset.type === 'radio');
const tvPresets = presets.filter((preset) => preset.type === 'tv');
useEffect(() => {
if (!selectedPreset && presets[0]) {
setSelectedPresetId(presets[0].id);
}
}, [presets, selectedPreset]);
const validateForm = (values: FormState) => {
const errors: FormErrors = {};
if (!values.title.trim()) {
errors.title = 'Give this preset a short title.';
}
if (!values.url.trim()) {
errors.url = 'Paste a stream, media, or embed URL.';
} else if (!isValidUrl(values.url.trim())) {
errors.url = 'Use a valid http:// or https:// URL.';
}
if (values.type === 'tv' && values.mode === 'embed' && !values.url.includes('embed') && !values.url.includes('youtube.com/watch?v=')) {
errors.mode = 'Use an embeddable URL or a YouTube watch link for TV embeds.';
}
return errors;
};
const handleFieldChange = <K extends keyof FormState>(field: K, value: FormState[K]) => {
setFeedbackMessage('');
setFormErrors((current) => ({ ...current, [field]: undefined }));
setFormState((current) => {
const nextState = { ...current, [field]: value };
if (field === 'type' && value === 'radio') {
nextState.mode = 'audio';
}
return nextState;
});
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const errors = validateForm(formState);
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return;
}
const nextMode = inferMode(formState.type, formState.url.trim(), formState.mode);
const nextPreset: MediaPreset = {
id: createId(),
type: formState.type,
title: formState.title.trim(),
url: normalizeTvUrl(formState.url.trim(), nextMode),
notes: formState.notes.trim(),
mode: nextMode,
};
const nextPresets = [nextPreset, ...presets];
setPresets(nextPresets);
setSelectedPresetId(nextPreset.id);
setFeedbackMessage(`${nextPreset.title} is now saved in this browser and loaded into the preview.`);
setFormErrors({});
setFormState({
...defaultFormState,
type: formState.type,
mode: formState.type === 'radio' ? 'audio' : 'video',
});
};
const handleDeletePreset = (presetId: string) => {
const nextPresets = presets.filter((preset) => preset.id !== presetId);
setPresets(nextPresets.length > 0 ? nextPresets : []);
setFeedbackMessage('Preset removed from this browser.');
if (selectedPresetId === presetId) {
setSelectedPresetId(nextPresets[0]?.id ?? '');
}
};
return (
<>
<Head>
<title>{getPageTitle('Interaction Hub')}</title>
<meta
name="description"
content="Browse the Modular Artificial Interaction switchboard and preview saved radio or TV presets."
/>
</Head>
<div className="min-h-screen bg-[#060916] text-white">
<div className="absolute inset-x-0 top-0 -z-0 h-[26rem] bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.22),_transparent_36%),radial-gradient(circle_at_top_right,_rgba(168,85,247,0.2),_transparent_30%),linear-gradient(180deg,_#0a1024,_#060916)]" />
<header className="border-b border-white/10 bg-[#060916]/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-5 md:flex-row md:items-center md:justify-between">
<div>
<Link href="/" className="inline-flex items-center gap-3 text-sm uppercase tracking-[0.3em] text-cyan-200/80 transition hover:text-white">
<BaseIcon path={mdiArrowLeft} size={18} />
Back to landing page
</Link>
<h1 className="mt-4 text-3xl font-semibold text-white md:text-4xl">Interaction Hub</h1>
<p className="mt-3 max-w-3xl text-sm leading-7 text-slate-300 md:text-base">
Browse the public tool switchboard, launch your external destinations, and test radio or TV presets without leaving the page.
</p>
</div>
<div className="flex flex-wrap gap-3">
<BaseButton href="/login" label="Admin interface" color="whiteDark" className="border-white/10" />
<BaseButton href="/dashboard" label="Dashboard" color="info" />
</div>
</div>
</header>
<main className="mx-auto grid max-w-7xl gap-6 px-6 py-8 xl:grid-cols-[1.05fr_0.95fr]">
<div className="space-y-6">
<CardBox className="border border-white/10 bg-slate-950/80 shadow-[0_20px_50px_rgba(15,23,42,0.35)]">
<div className="flex flex-col gap-4 border-b border-white/10 pb-5 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-cyan-200/70">Public switchboard</p>
<h2 className="mt-3 text-2xl font-semibold text-white">Launch your existing modules</h2>
</div>
<div className="rounded-full border border-emerald-400/30 bg-emerald-400/10 px-4 py-2 text-xs uppercase tracking-[0.22em] text-emerald-200">
{modularInteractionLinks.length} live destinations
</div>
</div>
<div className="mt-6 space-y-6">
{Object.entries(groupedLinks).map(([key, items]) => {
const meta = sectionMeta[key];
return (
<section key={key} className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/5 text-cyan-200">
<BaseIcon path={meta.icon} size={20} />
</div>
<div>
<h3 className="text-lg font-medium text-white">{meta.title}</h3>
<p className="text-sm text-slate-400">{meta.subtitle}</p>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
{items.map((item) => (
<a
key={item.id}
href={item.url}
target="_blank"
rel="noreferrer"
className={`group overflow-hidden rounded-[1.5rem] border border-white/10 bg-gradient-to-br ${item.accent} p-[1px] transition hover:-translate-y-1 hover:border-cyan-300/30`}
>
<div className="flex h-full flex-col rounded-[1.45rem] bg-slate-950/90 p-5">
<div className="flex items-center justify-between gap-3">
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] uppercase tracking-[0.2em] text-slate-300">
{item.eyebrow}
</span>
<BaseIcon path={mdiOpenInNew} size={18} className="text-slate-400 transition group-hover:text-white" />
</div>
<h4 className="mt-5 text-lg font-medium text-white">{item.title}</h4>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.summary}</p>
</div>
</a>
))}
</div>
</section>
);
})}
</div>
</CardBox>
<CardBox className="border border-white/10 bg-slate-950/80">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-fuchsia-500/10 text-fuchsia-200">
<BaseIcon path={mdiPlus} size={20} />
</div>
<div>
<h2 className="text-2xl font-semibold text-white">Preset studio</h2>
<p className="text-sm text-slate-400">Save a radio or TV source locally and push it straight into the preview.</p>
</div>
</div>
<form className="mt-6 space-y-5" onSubmit={handleSubmit}>
<div className="grid gap-3 sm:grid-cols-2">
{([
{ label: 'Radio', value: 'radio' },
{ label: 'TV', value: 'tv' },
] as const).map((option) => {
const isActive = formState.type === option.value;
return (
<button
key={option.value}
type="button"
onClick={() => handleFieldChange('type', option.value)}
className={`rounded-2xl border px-4 py-3 text-left transition ${
isActive
? 'border-cyan-300/40 bg-cyan-400/10 text-white'
: 'border-white/10 bg-white/[0.03] text-slate-300 hover:border-white/20'
}`}
>
<span className="text-sm uppercase tracking-[0.2em]">{option.label}</span>
</button>
);
})}
</div>
<div className="grid gap-5 md:grid-cols-2">
<div>
<label className="mb-2 block text-sm font-medium text-slate-200">Preset title</label>
<input
value={formState.title}
onChange={(event) => handleFieldChange('title', event.target.value)}
placeholder="My community radio"
className="w-full rounded-2xl border border-white/10 bg-slate-900 px-4 py-3 text-white outline-none transition focus:border-cyan-300/40"
/>
{formErrors.title ? <p className="mt-2 text-sm text-rose-300">{formErrors.title}</p> : null}
</div>
<div>
<label className="mb-2 block text-sm font-medium text-slate-200">
{formState.type === 'radio' ? 'Audio stream URL' : 'Video or embed URL'}
</label>
<input
value={formState.url}
onChange={(event) => handleFieldChange('url', event.target.value)}
placeholder={
formState.type === 'radio'
? 'https://example.com/live.mp3'
: 'https://example.com/embed/live or https://example.com/video.mp4'
}
className="w-full rounded-2xl border border-white/10 bg-slate-900 px-4 py-3 text-white outline-none transition focus:border-cyan-300/40"
/>
{formErrors.url ? <p className="mt-2 text-sm text-rose-300">{formErrors.url}</p> : null}
</div>
</div>
{formState.type === 'tv' ? (
<div>
<label className="mb-2 block text-sm font-medium text-slate-200">TV mode</label>
<div className="grid gap-3 sm:grid-cols-2">
{([
{ label: 'Direct video', value: 'video' },
{ label: 'Embed iframe', value: 'embed' },
] as const).map((option) => {
const isActive = formState.mode === option.value;
return (
<button
key={option.value}
type="button"
onClick={() => handleFieldChange('mode', option.value)}
className={`rounded-2xl border px-4 py-3 text-left transition ${
isActive
? 'border-fuchsia-300/40 bg-fuchsia-400/10 text-white'
: 'border-white/10 bg-white/[0.03] text-slate-300 hover:border-white/20'
}`}
>
<span className="text-sm uppercase tracking-[0.2em]">{option.label}</span>
</button>
);
})}
</div>
{formErrors.mode ? <p className="mt-2 text-sm text-rose-300">{formErrors.mode}</p> : null}
</div>
) : null}
<div>
<label className="mb-2 block text-sm font-medium text-slate-200">Notes</label>
<textarea
value={formState.notes}
onChange={(event) => handleFieldChange('notes', event.target.value)}
placeholder="Optional context for this preset"
className="min-h-[120px] w-full rounded-2xl border border-white/10 bg-slate-900 px-4 py-3 text-white outline-none transition focus:border-cyan-300/40"
/>
</div>
<div className="flex flex-wrap items-center gap-4">
<BaseButton type="submit" label="Save preset" icon={mdiPlus} color="info" />
<p className="text-sm text-slate-400">Presets are stored in this browser only for now.</p>
</div>
{feedbackMessage ? (
<div className="rounded-2xl border border-emerald-400/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-100">
{feedbackMessage}
</div>
) : null}
</form>
</CardBox>
</div>
<div className="space-y-6">
<CardBox className="border border-white/10 bg-slate-950/80 shadow-[0_20px_50px_rgba(15,23,42,0.35)]">
<div className="flex flex-col gap-4 border-b border-white/10 pb-5 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-cyan-200/70">Live preview</p>
<h2 className="mt-3 text-2xl font-semibold text-white">Media deck</h2>
</div>
<div className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs uppercase tracking-[0.22em] text-slate-300">
{selectedPreset ? getPreviewLabel(selectedPreset) : 'Awaiting selection'}
</div>
</div>
<div className="mt-6 grid gap-6">
<div className="overflow-hidden rounded-[1.75rem] border border-white/10 bg-gradient-to-br from-cyan-500/12 via-slate-950 to-slate-950 p-5">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-cyan-400/10 text-cyan-200">
<BaseIcon path={mdiRadioTower} size={22} />
</div>
<div>
<h3 className="text-lg font-medium text-white">Radio widget</h3>
<p className="text-sm text-slate-400">Audio presets and placeholder radio streams</p>
</div>
</div>
{selectedPreset?.type === 'radio' ? (
<div className="space-y-4">
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<p className="text-xs uppercase tracking-[0.22em] text-cyan-200/70">Now loaded</p>
<h4 className="mt-2 text-xl font-medium text-white">{selectedPreset.title}</h4>
{selectedPreset.notes ? <p className="mt-2 text-sm leading-7 text-slate-300">{selectedPreset.notes}</p> : null}
</div>
<audio key={selectedPreset.id} controls className="w-full">
<source src={selectedPreset.url} />
Your browser does not support the audio element.
</audio>
</div>
) : (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] p-5 text-sm leading-7 text-slate-300">
Select a radio preset from the list below, or add a new one in the preset studio.
</div>
)}
</div>
<div className="overflow-hidden rounded-[1.75rem] border border-white/10 bg-gradient-to-br from-fuchsia-500/12 via-slate-950 to-slate-950 p-5">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-fuchsia-400/10 text-fuchsia-200">
<BaseIcon path={mdiTelevisionPlay} size={22} />
</div>
<div>
<h3 className="text-lg font-medium text-white">TV widget</h3>
<p className="text-sm text-slate-400">Video or embed presets displayed in-page</p>
</div>
</div>
{selectedPreset?.type === 'tv' ? (
<div className="space-y-4">
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<p className="text-xs uppercase tracking-[0.22em] text-fuchsia-200/70">Now loaded</p>
<h4 className="mt-2 text-xl font-medium text-white">{selectedPreset.title}</h4>
{selectedPreset.notes ? <p className="mt-2 text-sm leading-7 text-slate-300">{selectedPreset.notes}</p> : null}
</div>
{selectedPreset.mode === 'embed' ? (
<div className="overflow-hidden rounded-[1.5rem] border border-white/10 bg-black/30">
<iframe
key={selectedPreset.id}
src={selectedPreset.url}
title={selectedPreset.title}
className="aspect-video w-full"
allow="autoplay; encrypted-media; picture-in-picture"
allowFullScreen
/>
</div>
) : (
<video
key={selectedPreset.id}
controls
className="aspect-video w-full overflow-hidden rounded-[1.5rem] border border-white/10 bg-black/30"
>
<source src={selectedPreset.url} />
Your browser does not support the video tag.
</video>
)}
</div>
) : (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] p-5 text-sm leading-7 text-slate-300">
Select a TV preset from the list below, or add a new one in the preset studio.
</div>
)}
</div>
</div>
</CardBox>
<CardBox className="border border-white/10 bg-slate-950/80">
<div className="flex flex-col gap-4 border-b border-white/10 pb-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl font-semibold text-white">Saved presets</h2>
<p className="mt-2 text-sm text-slate-400">Click any card to load it into the radio or TV preview above.</p>
</div>
<div className="text-sm text-slate-400">
{radioPresets.length} radio · {tvPresets.length} TV
</div>
</div>
<div className="mt-6 space-y-4">
{presets.length === 0 ? (
<div className="rounded-[1.5rem] border border-dashed border-white/10 bg-white/[0.03] p-6 text-sm leading-7 text-slate-300">
No presets saved yet. Start by adding a radio or TV source in the preset studio.
</div>
) : (
presets.map((preset) => {
const isActive = preset.id === selectedPreset?.id;
return (
<div
key={preset.id}
className={`rounded-[1.5rem] border p-5 transition ${
isActive
? 'border-cyan-300/35 bg-cyan-400/10'
: 'border-white/10 bg-white/[0.03] hover:border-white/20'
}`}
>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<button
type="button"
onClick={() => setSelectedPresetId(preset.id)}
className="text-left"
>
<div className="flex items-center gap-3">
<span
className={`rounded-full px-3 py-1 text-xs uppercase tracking-[0.2em] ${
preset.type === 'radio'
? 'bg-cyan-400/10 text-cyan-200'
: 'bg-fuchsia-400/10 text-fuchsia-200'
}`}
>
{preset.type}
</span>
{preset.isSample ? (
<span className="rounded-full border border-white/10 px-3 py-1 text-[11px] uppercase tracking-[0.2em] text-slate-400">
sample
</span>
) : null}
</div>
<h3 className="mt-4 text-lg font-medium text-white">{preset.title}</h3>
<p className="mt-2 break-all text-sm text-slate-400">{preset.url}</p>
{preset.notes ? <p className="mt-3 text-sm leading-7 text-slate-300">{preset.notes}</p> : null}
</button>
<button
type="button"
onClick={() => handleDeletePreset(preset.id)}
className="inline-flex items-center gap-2 self-start rounded-full border border-white/10 px-4 py-2 text-sm text-slate-300 transition hover:border-rose-300/40 hover:text-rose-200"
>
<BaseIcon path={mdiDeleteOutline} size={18} />
Remove
</button>
</div>
</div>
);
})
)}
</div>
</CardBox>
<CardBox className="border border-white/10 bg-slate-950/80">
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-emerald-400/10 text-emerald-200">
<BaseIcon path={mdiViewDashboardOutline} size={22} />
</div>
<div>
<h2 className="text-2xl font-semibold text-white">Admin follow-through</h2>
<p className="text-sm text-slate-400">When you are ready, continue from the public experience into the protected control room.</p>
</div>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{adminShortcuts.map((item) => (
<Link
key={item.title}
href={item.href}
className="rounded-[1.5rem] border border-white/10 bg-white/[0.03] p-5 transition hover:-translate-y-1 hover:border-cyan-300/30"
>
<h3 className="text-lg font-medium text-white">{item.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.summary}</p>
</Link>
))}
</div>
</CardBox>
</div>
</main>
</div>
</>
);
}
InteractionHubPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};