diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index eb155e3..fb0fca2 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -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' diff --git a/frontend/src/helpers/modularInteractionHub.ts b/frontend/src/helpers/modularInteractionHub.ts new file mode 100644 index 0000000..85ee5d1 --- /dev/null +++ b/frontend/src/helpers/modularInteractionHub.ts @@ -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.', +]; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -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' diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 3d7301f..b8c2ff0 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -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', diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index ffe2474..9ecc6cb 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; +const statCards = [ + { value: '2', label: 'AI experiences' }, + { value: '4', label: 'Fillout entry forms' }, + { value: '2', label: 'Media widgets' }, +]; +export default function HomePage() { return ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('Modular Artificial Interaction')} + - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+
+ +
+
+ +
+ +
+
+

Modular Artificial

+

Interaction Hub

+
+ + + + +
+ +
- - - +
+
- - -
-
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+
+
+
+ + Beautiful public front door for AI, links, and media +
-
+
+

+ Present Modular Artificial Interaction as one living experience. +

+

+ 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. +

+
+ +
+ + +
+ +
+ {statCards.map((item) => ( +
+
{item.value}
+
+ {item.label} +
+
+ ))} +
+
+ +
+
+
+
+
+

Live concept board

+

How the experience feels

+
+
+ Public ready +
+
+ +
+ {hubHighlights.map((item, index) => ( +
+
+
+ 0{index + 1} +
+

{item.title}

+
+

{item.description}

+
+ ))} +
+
+
+ + +
+
+
+
+

Concept

+

+ A modular stack for interaction, intake, and media. +

+

+ 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. +

+
+ +
+ {experienceSteps.map((step, index) => ( +
+
+ Step {index + 1} +
+

{step}

+
+ ))} +
+
+
+
+ +
+
+

Modules

+

+ The first branded slice is more than a landing page. +

+
+ +
+ {moduleCards.map((item) => ( +
+
+ +
+

{item.title}

+

{item.description}

+
+ ))} +
+
+ +
+
+
+

Switchboard preview

+

+ Curated launch cards for the tools you already have. +

+
+ +
+ + +
+ +
+
+
+
+ +

Radio widget

+
+

In-page listening for your audience

+

+ 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. +

+
+
+
+ +

TV widget

+
+

Flexible embed or video preview

+

+ Visitors can stay on your site while watching a sample video or a future live embed, + giving the experience a richer broadcast identity. +

+
+
+
+ +
+
+
+

Admin continuity

+

+ Keep the admin power you already have. +

+

+ 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. +

+
+ +
+ {adminShortcuts.map((item) => ( + +

{item.title}

+

{item.summary}

+ + ))} +
+
+
+ + +
+
+

© 2026 Modular Artificial Interaction Hub. Built as a public AI + media launch surface.

+
+ + Open hub + + + Login + + + Privacy policy + + + Terms + +
+
+
+
+ ); } -Starter.getLayout = function getLayout(page: ReactElement) { +HomePage.getLayout = function getLayout(page: ReactElement) { return {page}; }; - diff --git a/frontend/src/pages/interaction-hub.tsx b/frontend/src/pages/interaction-hub.tsx new file mode 100644 index 0000000..b0a3e54 --- /dev/null +++ b/frontend/src/pages/interaction-hub.tsx @@ -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>; + +const defaultFormState: FormState = { + type: 'radio', + title: '', + url: '', + notes: '', + mode: 'audio', +}; + +const sectionMeta: Record = { + 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(starterMediaPresets); + const [selectedPresetId, setSelectedPresetId] = useState(starterMediaPresets[0].id); + const [formState, setFormState] = useState(defaultFormState); + const [formErrors, setFormErrors] = useState({}); + 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>((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 = (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) => { + 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 ( + <> + + {getPageTitle('Interaction Hub')} + + + +
+
+ +
+
+
+ + + Back to landing page + +

Interaction Hub

+

+ Browse the public tool switchboard, launch your external destinations, and test radio or TV presets without leaving the page. +

+
+ +
+ + +
+
+
+ +
+
+ +
+
+

Public switchboard

+

Launch your existing modules

+
+
+ {modularInteractionLinks.length} live destinations +
+
+ +
+ {Object.entries(groupedLinks).map(([key, items]) => { + const meta = sectionMeta[key]; + + return ( +
+
+
+ +
+
+

{meta.title}

+

{meta.subtitle}

+
+
+ + +
+ ); + })} +
+
+ + +
+
+ +
+
+

Preset studio

+

Save a radio or TV source locally and push it straight into the preview.

+
+
+ +
+
+ {([ + { label: 'Radio', value: 'radio' }, + { label: 'TV', value: 'tv' }, + ] as const).map((option) => { + const isActive = formState.type === option.value; + + return ( + + ); + })} +
+ +
+
+ + 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 ?

{formErrors.title}

: null} +
+ +
+ + 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 ?

{formErrors.url}

: null} +
+
+ + {formState.type === 'tv' ? ( +
+ +
+ {([ + { label: 'Direct video', value: 'video' }, + { label: 'Embed iframe', value: 'embed' }, + ] as const).map((option) => { + const isActive = formState.mode === option.value; + + return ( + + ); + })} +
+ {formErrors.mode ?

{formErrors.mode}

: null} +
+ ) : null} + +
+ +