diff --git a/src/components/AddItemModal/index.tsx b/src/components/AddItemModal/index.tsx index 01fedb7ad..e5d33b609 100644 --- a/src/components/AddItemModal/index.tsx +++ b/src/components/AddItemModal/index.tsx @@ -145,7 +145,7 @@ export const AddItemModal = () => { }, } - addNewNode({ nodes: [node], links: [] }) + addNewNode({ nodes: [node], edges: [] }) setSelectedNode(node) } diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 3f871a315..4e0464179 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -161,10 +161,14 @@ export const App = () => { timerRef.current = setTimeout(() => { // Combine all queued data into a single update - const batchedData = { ...queueRef.current } + if (queueRef.current) { + const { nodes: newNodes, edges: newEdges } = queueRef.current + const batchedData = { nodes: newNodes, edges: newEdges } - queueRef.current = { nodes: [], edges: [] } // Reset the queue - addNewNode(batchedData) // Call the original addNewNode function with batched data + queueRef.current = { nodes: [], edges: [] } + + addNewNode(batchedData) + } }, 3000) // Adjust delay as necessary }, [addNewNode, isFetching], @@ -293,8 +297,6 @@ export const App = () => { } ws.onmessage = (event) => { - console.log('Message from server:', event.data) - const data = JSON.parse(event.data) if (data.type === 'ping') { @@ -313,10 +315,6 @@ export const App = () => { ws.onerror = (error) => { console.error('WebSocket error:', error) } - - ws.onclose = () => { - console.log('WebSocket connection closed') - } }, [runningProjectId, setRunningProjectMessages]) useEffect(() => { diff --git a/src/components/AppContainer/index.tsx b/src/components/AppContainer/index.tsx index a9678ab2d..c9f8ea3d9 100644 --- a/src/components/AppContainer/index.tsx +++ b/src/components/AppContainer/index.tsx @@ -5,20 +5,23 @@ import { AppProviders } from '../App/Providers' import { AuthGuard } from '../Auth' const LazyApp = lazy(() => import('../App').then(({ App }) => ({ default: App }))) +const LazyMindSet = lazy(() => import('../mindset').then(({ MindSet }) => ({ default: MindSet }))) export const AppContainer = () => { const App = + const MindSet = + + const path = window.location?.hostname === 'graphmindset.sphinx.chat' ? '/' : '/mindset' return ( Loading...}> - - - - - - - + + + {App}} path="/" /> + {App}} path="/search" /> + {App}} path="*" /> + diff --git a/src/components/Auth/index.tsx b/src/components/Auth/index.tsx index 5fce6b550..d1eebbb76 100644 --- a/src/components/Auth/index.tsx +++ b/src/components/Auth/index.tsx @@ -113,7 +113,7 @@ export const AuthGuard = ({ children }: PropsWithChildren) => { await handleAuth() } catch (error) { - console.log(error) + console.error(error) } } diff --git a/src/components/ModalsContainer/AddNodeEdgeModal/Body/index.tsx b/src/components/ModalsContainer/AddNodeEdgeModal/Body/index.tsx index c184629db..69b8b4d04 100644 --- a/src/components/ModalsContainer/AddNodeEdgeModal/Body/index.tsx +++ b/src/components/ModalsContainer/AddNodeEdgeModal/Body/index.tsx @@ -76,11 +76,6 @@ export const Body = () => { : { to: nodeFrom.ref_id, from: selectedToNode?.ref_id }), }) - const { ref_id: id } = nodeFrom - const { ref_id: selectedId } = selectedToNode - - console.log(id, selectedId) - closeHandler() } catch (error) { console.warn(error) diff --git a/src/components/ModalsContainer/CreateBountyModal/CreateBounty/index.tsx b/src/components/ModalsContainer/CreateBountyModal/CreateBounty/index.tsx index 686b1175a..cc3996bcb 100644 --- a/src/components/ModalsContainer/CreateBountyModal/CreateBounty/index.tsx +++ b/src/components/ModalsContainer/CreateBountyModal/CreateBounty/index.tsx @@ -52,7 +52,7 @@ export const CreateBounty: FC = ({ errMessage, handleClose }) => { setOptions(newOptions) } } catch (error) { - console.log('Error from get user details: ', error) + console.error('Error from get user details: ', error) } } diff --git a/src/components/ModalsContainer/EditNodeNameModal/Body/index.tsx b/src/components/ModalsContainer/EditNodeNameModal/Body/index.tsx index 8f1a22810..7db7173a7 100644 --- a/src/components/ModalsContainer/EditNodeNameModal/Body/index.tsx +++ b/src/components/ModalsContainer/EditNodeNameModal/Body/index.tsx @@ -1,5 +1,5 @@ import { Button, Skeleton } from '@mui/material' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { ClipLoader } from 'react-spinners' import styled from 'styled-components' @@ -61,7 +61,7 @@ export const Body = () => { setActualTopicNode(node) } catch (error) { - console.log(error) + console.error(error) } finally { setTopicIsLoading(false) } diff --git a/src/components/ModalsContainer/RemoveNodeModal/Body/index.tsx b/src/components/ModalsContainer/RemoveNodeModal/Body/index.tsx index 650afdabe..c82f9d466 100644 --- a/src/components/ModalsContainer/RemoveNodeModal/Body/index.tsx +++ b/src/components/ModalsContainer/RemoveNodeModal/Body/index.tsx @@ -53,7 +53,7 @@ export const Body = () => { setActualNode(selectedNode) } } catch (error) { - console.log(error) + console.error(error) } finally { setTopicIsLoading(false) } diff --git a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx index fadc7dae5..e392edd48 100644 --- a/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx +++ b/src/components/Universe/Graph/Cubes/SelectionDataNodes/index.tsx @@ -118,7 +118,7 @@ export const SelectionDataNodes = memo(() => { {selectionGraphData?.nodes.map((node) => ( - + ))} diff --git a/src/components/Universe/Graph/Cubes/Text/index.tsx b/src/components/Universe/Graph/Cubes/Text/index.tsx index e257a4373..64f3b8808 100644 --- a/src/components/Universe/Graph/Cubes/Text/index.tsx +++ b/src/components/Universe/Graph/Cubes/Text/index.tsx @@ -1,10 +1,10 @@ import { Billboard, Plane, Svg, Text } from '@react-three/drei' import { useFrame } from '@react-three/fiber' -import { memo, useMemo, useRef } from 'react' +import { memo, useRef } from 'react' import { Mesh, MeshBasicMaterial, Vector3 } from 'three' import { Icons } from '~/components/Icons' import { useNodeTypes } from '~/stores/useDataStore' -import { useGraphStore, useSelectedNode, useSelectedNodeRelativeIds } from '~/stores/useGraphStore' +import { useGraphStore } from '~/stores/useGraphStore' import { useSchemaStore } from '~/stores/useSchemaStore' import { NodeExtended } from '~/types' import { colors } from '~/utils/colors' @@ -46,7 +46,6 @@ const COLORS_MAP = [ type Props = { node: NodeExtended hide?: boolean - isHovered: boolean ignoreDistance: boolean } @@ -68,23 +67,20 @@ function splitStringIntoThreeParts(text: string): string { return `${firstPart}\n${secondPart}\n${thirdPart}` } -export const TextNode = memo(({ node, hide, isHovered, ignoreDistance }: Props) => { +export const TextNode = memo(({ node, hide, ignoreDistance }: Props) => { const svgRef = useRef(null) const ringRef = useRef(null) const circleRef = useRef(null) - const selectedNode = useSelectedNode() const nodePositionRef = useRef(new Vector3()) const { texture } = useTexture(node.properties?.image_url || '') - const selectedNodeRelativeIds = useSelectedNodeRelativeIds() - const isRelative = selectedNodeRelativeIds.includes(node?.ref_id || '') - const isSelected = !!selectedNode && selectedNode?.ref_id === node.ref_id - const showSelectionGraph = useGraphStore((s) => s.showSelectionGraph) const { normalizedSchemasByType } = useSchemaStore((s) => s) useFrame(({ camera, clock }) => { + const { selectedNode, hoveredNode, activeEdge } = useGraphStore.getState() + const checkDistance = () => { const nodePosition = nodePositionRef.current.setFromMatrixPosition(ringRef.current!.matrixWorld) @@ -95,55 +91,36 @@ export const TextNode = memo(({ node, hide, isHovered, ignoreDistance }: Props) // Set visibility based on distance } - if (isHovered) { + const isActive = + node.ref_id === selectedNode?.ref_id || + node.ref_id === hoveredNode?.ref_id || + activeEdge?.target === node.ref_id || + activeEdge?.source === node.ref_id + + if (isActive) { if (ringRef.current) { ringRef.current.visible = true } - const scale = 1 + 0.2 * Math.sin(clock.getElapsedTime() * 2) // Adjust frequency and amplitude + const scale = 3 + 0.2 * Math.sin(clock.getElapsedTime() * 2) // Adjust frequency and amplitude if (circleRef.current) { + circleRef.current.visible = true circleRef.current.scale.set(scale, scale, scale) } return } + if (circleRef.current) { + circleRef.current.visible = false + } + checkDistance() }) const nodeTypes = useNodeTypes() - const textScale = useMemo(() => { - if (!node.name) { - return 0 - } - - let scale = (node.edge_count || 1) * 20 - - if (showSelectionGraph && isSelected) { - scale = 40 - } else if (!isSelected && isRelative) { - scale = 0 - } - - const nodeScale = scale / Math.sqrt(node.name.length) - - return Math.min(Math.max(nodeScale, 20), 30) - }, [node.edge_count, node.name, isSelected, isRelative, showSelectionGraph]) - - const fillOpacity = useMemo(() => { - if (selectedNode && !isSelected) { - return 0.2 - } - - if (!isHovered) { - return 0.2 - } - - return 1 - }, [isSelected, selectedNode, isHovered]) - const primaryColor = normalizedSchemasByType[node.node_type]?.primary_color const primaryIcon = normalizedSchemasByType[node.node_type]?.icon @@ -161,12 +138,11 @@ export const TextNode = memo(({ node, hide, isHovered, ignoreDistance }: Props) return ( - {isHovered ? ( - - - - - ) : null} + + + + + {node.properties?.image_url && node.node_type === 'Person' && texture ? ( diff --git a/src/components/Universe/Graph/Cubes/index.tsx b/src/components/Universe/Graph/Cubes/index.tsx index ac96d9c86..c7517eccb 100644 --- a/src/components/Universe/Graph/Cubes/index.tsx +++ b/src/components/Universe/Graph/Cubes/index.tsx @@ -117,13 +117,7 @@ export const Cubes = memo(() => { - + ) })} diff --git a/src/components/Universe/Graph/UI/NodeControls/index.tsx b/src/components/Universe/Graph/UI/NodeControls/index.tsx index d66fa1972..7b3fc5cb2 100644 --- a/src/components/Universe/Graph/UI/NodeControls/index.tsx +++ b/src/components/Universe/Graph/UI/NodeControls/index.tsx @@ -56,7 +56,7 @@ export const NodeControls = memo(() => { } } } catch (error) { - console.log(error) + console.error(error) } }, [addNewNode, selectedNode?.ref_id, selectionGraphData.nodes.length]) diff --git a/src/components/mindset/components/Header/index.tsx b/src/components/mindset/components/Header/index.tsx new file mode 100644 index 000000000..280530014 --- /dev/null +++ b/src/components/mindset/components/Header/index.tsx @@ -0,0 +1,61 @@ +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { Text } from '~/components/common/Text' +import { colors } from '~/utils/colors' +import { Logo } from '../Icon/Logo' + +export const Header = () => ( + + + + + + + Graph Mindset + +) + +const Head = styled(Flex).attrs({ + align: 'center', + direction: 'row', + grow: 1, + justify: 'flex-start', +})` + height: 64px; + padding: 20px 23px; + gap: 0px; + z-index: 50; + position: relative; +` + +const LogoButton = styled(Flex)` + align-items: center; + justify-content: center; + cursor: pointer; +` + +const IconWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 30px; + height: 27px; + color: ${colors.white}; + } +` + +const StyledText = styled(Text)` + width: 127px; + height: 24px; + color: ${colors.white}; + font-family: Barlow; + font-size: 22px; + font-style: normal; + font-weight: 700; + line-height: 24px; + letter-spacing: 0.22px; + margin-left: 16px; + white-space: nowrap; +` diff --git a/src/components/mindset/components/Icon/ChevronRight.tsx b/src/components/mindset/components/Icon/ChevronRight.tsx new file mode 100644 index 000000000..e6f7e4aaa --- /dev/null +++ b/src/components/mindset/components/Icon/ChevronRight.tsx @@ -0,0 +1,13 @@ +/* eslint-disable */ +import React from 'react' + +export const ChevronRight: React.FC> = (props) => ( + + + +) + +export default ChevronRight diff --git a/src/components/mindset/components/Icon/Logo.tsx b/src/components/mindset/components/Icon/Logo.tsx new file mode 100644 index 000000000..70b13f9be --- /dev/null +++ b/src/components/mindset/components/Icon/Logo.tsx @@ -0,0 +1,49 @@ +/* eslint-disable */ +import React from 'react' + +export const Logo: React.FC> = (props) => ( + + + + + + + + + + + + +) + +export default Logo diff --git a/src/components/mindset/components/LandingPage/index.tsx b/src/components/mindset/components/LandingPage/index.tsx new file mode 100644 index 000000000..7550b5bfd --- /dev/null +++ b/src/components/mindset/components/LandingPage/index.tsx @@ -0,0 +1,180 @@ +import { useState } from 'react' +import { FieldValues } from 'react-hook-form' +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { NODE_ADD_ERROR } from '~/constants' +import { api } from '~/network/api' +import { useDataStore } from '~/stores/useDataStore' +import { useMindsetStore } from '~/stores/useMindsetStore' +import { SubmitErrRes } from '~/types' +import { colors } from '~/utils/colors' +import { ChevronRight } from '../Icon/ChevronRight' +import { isValidMediaUrl } from './utils' + +export type FormData = { + input: string + inputType: string + source: string + longitude: string + latitude: string +} + +const handleSubmitForm = async (data: FieldValues): Promise => { + const endPoint = 'add_node' + + const body: { [index: string]: unknown } = {} + + body.media_url = data.source + body.content_type = 'audio_video' + + const res: SubmitErrRes = await api.post(`/${endPoint}`, JSON.stringify(body)) + + if (res.error) { + const { message } = res.error + + throw new Error(message) + } + + return res +} + +export const LandingPage = () => { + const [inputValue, setInputValue] = useState('') + const [error, setError] = useState(false) + const [requestError, setRequestError] = useState('') + const { setRunningProjectId } = useDataStore((s) => s) + const { setSelectedEpisodeId, setSelectedEpisodeLink } = useMindsetStore((s) => s) + + const handleInputChange = (e: React.ChangeEvent) => { + const { value } = e.target + + setInputValue(value) + setError(value !== '' && !isValidMediaUrl(value)) + } + + const handleSubmit = async () => { + if (isValidMediaUrl(inputValue)) { + try { + const res = await handleSubmitForm({ source: inputValue }) + + if (res.data.project_id) { + setRunningProjectId(res.data.project_id) + } + + if (res.data.ref_id) { + setSelectedEpisodeId(res.data.ref_id) + setSelectedEpisodeLink(inputValue) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + let errorMessage = NODE_ADD_ERROR + + if (err?.status === 400) { + const res = await err.json() + + errorMessage = res.errorCode || res?.status || NODE_ADD_ERROR + + if (res.data.ref_id) { + setSelectedEpisodeId(res.data.ref_id) + setSelectedEpisodeLink(inputValue) + } + } else if (err instanceof Error) { + errorMessage = err.message + } + + setRequestError(String(errorMessage)) + } + } + } + + return ( + + Ideas have shapes + + e.key === 'Enter' && handleSubmit()} + placeholder="Paste podcast or video link" + value={inputValue} + /> + + + + + {requestError &&
{requestError}
} +
+ ) +} + +const Wrapper = styled(Flex)` + background: #16161de3; + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + color: #fff; + align-items: center; + justify-content: center; + font-size: 32px; + font-style: normal; + font-weight: 700; + line-height: 16px; + font-family: 'Barlow'; + z-index: 40; +` + +const Title = styled(Flex)` + color: ${colors.white}; + font-family: Barlow; + font-size: 32px; + font-weight: 700; + margin-bottom: 40px; + text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25); +` + +const Input = styled.input<{ error?: boolean }>` + width: 100%; + max-width: 450px; + padding: 12px 28px 12px 16px; + border-radius: 100px; + border: 1px solid ${(props) => (props.error ? 'red' : colors.DIVIDER_4)}; + background: ${colors.INPUT_BG}; + color: ${colors.white}; + font-family: Barlow; + font-size: 16px; + + &::placeholder { + color: ${colors.INPUT_PLACEHOLDER}; + } + + &:focus { + outline: none; + border-color: ${(props) => (props.error ? 'red' : colors.primaryBlue)}; + } +` + +const InputWrapper = styled.div` + position: relative; + width: 450px; + display: flex; + align-items: center; +` + +const IconWrapper = styled.div<{ error?: boolean }>` + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + color: ${colors.white}; + font-size: 20px; + cursor: ${(props) => (props.error ? 'not-allowed' : 'pointer')}; + + svg { + width: 8px; + height: 17px; + color: ${colors.GRAY6}; + } +` diff --git a/src/components/mindset/components/LandingPage/utils/index.tsx b/src/components/mindset/components/LandingPage/utils/index.tsx new file mode 100644 index 000000000..2c5b80d9a --- /dev/null +++ b/src/components/mindset/components/LandingPage/utils/index.tsx @@ -0,0 +1,47 @@ +const protocol = /^(https?:\/\/)/ +const subDomain = /(www\.)?/ +const rootDomain = /[\w-]+(\.[\w-]+)*/ +const topLevelDomains = /(?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61})[a-zA-Z0-9](?:\.[a-zA-Z]{2,})/ +const path = /(\/[^\s?]*)?/ +const query = /(\?[^\s]*)?/ + +const youtubeRegex = /(https?:\/\/)?(www\.)?youtube\.com\/watch\?v=([A-Za-z0-9_-]+)/ +const youtubeLiveRegex = /(https?:\/\/)?(www\.)?youtube\.com\/live\/([A-Za-z0-9_-]+)/ +const youtubeShortRegex = /(https?:\/\/)?(www\.)?youtu\.be\/([A-Za-z0-9_-]+)/ +const mp3Regex = /(https?:\/\/)?([A-Za-z0-9_-]+)\.mp3/ + +const urlRegex = new RegExp( + `${protocol.source}${subDomain.source}${rootDomain.source}${topLevelDomains.source}?${path.source}${query.source}$`, + 'i', +) + +export const validateUrl = (input: string): boolean => { + try { + const match = input?.match(urlRegex) + + if (!match) { + return false + } + + const url = new URL(input) + const domain = url.hostname + + if (domain?.startsWith('www.')) { + return (domain?.match(/\./g) || []).length >= 2 + } + + return (domain?.match(/\./g) || []).length >= 1 + } catch { + return false + } +} + +export const isValidMediaUrl = (url: string): boolean => { + if (!validateUrl(url)) { + return false + } + + const mediaPatterns = [youtubeRegex, youtubeLiveRegex, youtubeShortRegex, mp3Regex] + + return mediaPatterns.some((pattern) => pattern.test(url)) +} diff --git a/src/components/mindset/components/Marker/index.tsx b/src/components/mindset/components/Marker/index.tsx new file mode 100644 index 000000000..0fd63daed --- /dev/null +++ b/src/components/mindset/components/Marker/index.tsx @@ -0,0 +1,54 @@ +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { useSchemaStore } from '~/stores/useSchemaStore' +import { colors } from '~/utils/colors' + +type Props = { + type: string +} + +type BadgeProps = { + iconStart: string + color: string + label: string +} + +export const Marker = ({ type }: Props) => { + const [normalizedSchemasByType] = useSchemaStore((s) => [s.normalizedSchemasByType]) + + const primaryColor = normalizedSchemasByType[type]?.primary_color + const primaryIcon = normalizedSchemasByType[type]?.icon + + const icon = primaryIcon ? `svg-icons/${primaryIcon}.svg` : null + + const badgeProps: Omit = { + iconStart: icon ?? 'thing_badge.svg', + color: primaryColor ?? colors.THING, + } + + return +} + +const Badge = ({ iconStart, color, label }: BadgeProps) => ( + + {label} + +) + +const EpisodeWrapper = styled(Flex).attrs({ + direction: 'row', +})<{ color: string }>` + cursor: pointer; + background: ${({ color }) => color}; + border-radius: 3px; + overflow: hidden; + justify-content: center; + align-items: center; + padding: 0 4px; + + .badge__img { + width: 10px; + height: 10px; + object-fit: contain; + } +` diff --git a/src/components/mindset/components/MediaPlayer/ToolBar/index.tsx b/src/components/mindset/components/MediaPlayer/ToolBar/index.tsx new file mode 100644 index 000000000..1ca5f3319 --- /dev/null +++ b/src/components/mindset/components/MediaPlayer/ToolBar/index.tsx @@ -0,0 +1,250 @@ +import { IconButton, Slider } from '@mui/material' +import { FC, useState } from 'react' +import styled from 'styled-components' +import ExitFullScreen from '~/components/Icons/ExitFullScreen' +import FullScreenIcon from '~/components/Icons/FullScreenIcon' +import PauseIcon from '~/components/Icons/PauseIcon' +import PlayIcon from '~/components/Icons/PlayIcon' +import VolumeIcon from '~/components/Icons/VolumeIcon' +import MuteVolumeIcon from '~/components/Icons/MuteVolumeIcon' +import { Flex } from '~/components/common/Flex' +import { colors } from '~/utils' +import { secondsToMediaTime } from '~/utils/secondsToMediaTime' + +type Props = { + isPlaying: boolean + isFullScreen: boolean + setIsPlaying: () => void + handleProgressChange: (_: Event, value: number | number[]) => void + handleVolumeChange: (_: Event, value: number | number[]) => void + playingTime: number + duration: number + onFullScreenClick: () => void + showToolbar: boolean +} + +export const Toolbar: FC = ({ + isPlaying, + isFullScreen, + setIsPlaying, + playingTime, + duration, + handleProgressChange, + handleVolumeChange, + onFullScreenClick, + showToolbar, +}) => { + const [volume, setVolume] = useState(0.5) + const [isMuted, setIsMuted] = useState(false) + const [previousVolume, setPreviousVolume] = useState(0.5) + + const volumeChangeHandler = (event: Event, value: number | number[]) => { + const newValue = Array.isArray(value) ? value[0] : value + + setVolume(newValue) + handleVolumeChange(event, newValue) + + if (isMuted) { + setIsMuted(false) + } + } + + const toggleMute = () => { + if (isMuted) { + setVolume(previousVolume) + handleVolumeChange(new Event('input'), previousVolume) + } else { + setPreviousVolume(volume) + setVolume(0) + handleVolumeChange(new Event('input'), 0) + } + + setIsMuted(!isMuted) + } + + return ( + + {(!showToolbar || isFullScreen) && ( + + )} + + + + {!isPlaying ? : } + + + {secondsToMediaTime(playingTime)} + / + {secondsToMediaTime(duration)} + + + + + + {isMuted ? ( + + + + ) : ( + + )} + + + + {!isFullScreen ? : } + + + + ) +} + +const Wrapper = styled(Flex)<{ showToolbar: boolean }>` + height: 60px; + padding: 12px 16px; + ${(props) => + props.showToolbar && + ` + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index:1; + background-color: rgba(0, 0, 0, 0.6); + `} + + &.error-wrapper { + color: ${colors.primaryRed}; + } +` + +const VolumeWrapper = styled.span`` + +const MuteVolumeWrapper = styled.span` + color: gray; +` + +const Action = styled(IconButton)` + && { + font-size: 36px; + padding: 2px; + margin-left: 8px; + } +` + +const VolumeControl = styled(Flex)` + height: 28px; + font-size: 26px; + border-radius: 200px; + color: ${colors.white}; + margin-left: auto; + + .volume-slider { + display: none; + color: ${colors.white}; + height: 3px; + .MuiSlider-track { + border: none; + } + .MuiSlider-thumb { + width: 2px; + height: 10px; + background-color: ${colors.white}; + &:before { + box-shadow: '0 4px 8px rgba(0,0,0,0.4)'; + } + &:hover, + &.Mui-focusVisible, + &.Mui-active { + box-shadow: none; + } + } + } + + &:hover { + background: rgba(42, 44, 55, 1); + .volume-slider { + width: 62px; + margin-right: 4px; + display: block; + } + } +` + +const Fullscreen = styled(Flex)` + cursor: pointer; + padding: 8px; + font-size: 32px; + color: #d9d9d9; +` + +const ProgressSlider = styled(Slider)<{ isFullScreen: boolean }>` + && { + z-index: 20; + color: ${colors.white}; + height: 3px; + width: calc(100% - 12px); + margin: ${(props) => (props.isFullScreen ? '80px auto' : '-12px auto')}; + box-sizing: border-box; + + ${(props) => + props.isFullScreen && + ` + width: calc(100% - 80px) + padding: 12px auto; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index:1; + `} + + .MuiSlider-track { + border: none; + } + .MuiSlider-thumb { + width: 10px; + height: 10px; + background-color: ${colors.white}; + &:before { + box-shadow: '0 4px 8px rgba(0,0,0,0.4)'; + } + &:hover, + &.Mui-focusVisible, + &.Mui-active { + box-shadow: none; + } + } + } +` + +const TimeStamp = styled(Flex)` + color: ${colors.white}; + font-size: 13px; + margin-left: 16px; + font-weight: 500; + + .separator { + color: ${colors.GRAY6}; + margin: 0 4px; + } + + .duration { + color: ${colors.GRAY6}; + } +` diff --git a/src/components/mindset/components/MediaPlayer/index.tsx b/src/components/mindset/components/MediaPlayer/index.tsx new file mode 100644 index 000000000..b2c85a582 --- /dev/null +++ b/src/components/mindset/components/MediaPlayer/index.tsx @@ -0,0 +1,284 @@ +import { memo, useEffect, useMemo, useRef, useState } from 'react' +import ReactPlayer from 'react-player' + +import styled from 'styled-components' +import { Avatar } from '~/components/common/Avatar' +import { Flex } from '~/components/common/Flex' +import { usePlayerStore } from '~/stores/usePlayerStore' +import { colors, videoTimeToSeconds } from '~/utils' + +import { useDataStore } from '~/stores/useDataStore' +import { useGraphStore, useSelectedNode } from '~/stores/useGraphStore' +import { Link } from '~/types' + +const findCurrentEdge = (sortedEdges: Link[], playerProgress: number): Link | null => { + // Sort edges by start (preprocessing step) + + let low = 0 + let high = sortedEdges.length - 1 + + while (low <= high) { + const mid = Math.floor((low + high) / 2) + const edge = sortedEdges[mid] + const { start, end } = edge as { start: number; end: number } + + if (playerProgress >= start && playerProgress <= end) { + return edge // Found the corresponding edge + } + + if (playerProgress < start) { + high = mid - 1 // Search in the left half + } else { + low = mid + 1 // Search in the right half + } + } + + return null // No matching edge found +} + +type FullScreenProps = { + isFullScreen: boolean +} + +type Props = { + mediaUrl: string +} + +const MediaPlayerComponent = ({ mediaUrl }: Props) => { + const playerRef = useRef(null) + const wrapperRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + const [isFullScreen, setIsFullScreen] = useState(false) + const [isMouseNearBottom, setIsMouseNearBottom] = useState(false) + const [status, setStatus] = useState<'buffering' | 'error' | 'ready'>('ready') + const [isReady, setIsReady] = useState(false) + const [NodeStartTime, setNodeStartTime] = useState('') + const [hasSeekedToStart, setHasSeekedToStart] = useState(false) + const selectedNode = useSelectedNode() + const { setActiveEdge } = useGraphStore((s) => s) + + const { dataInitial } = useDataStore((s) => s) + + useEffect(() => { + const timestamp = '00:00:00-00:12:00' + + const startTime = timestamp?.split('-')[0] as string + + setNodeStartTime(startTime as string) + }, [selectedNode]) + + const { + isPlaying, + playingTime, + setIsPlaying, + setPlayingTime, + setDuration, + playingNode, + volume, + setHasError, + resetPlayer, + isSeeking, + setIsSeeking, + } = usePlayerStore((s) => s) + + useEffect(() => () => resetPlayer(), [resetPlayer]) + + useEffect(() => { + if (playingNode && !isReady) { + setPlayingTime(0) + setDuration(0) + setIsReady(false) + setHasSeekedToStart(false) + } + }, [playingNode, setPlayingTime, setDuration, setIsReady, isReady]) + + useEffect(() => { + if (isSeeking && playerRef.current) { + playerRef.current.seekTo(playingTime, 'seconds') + setIsSeeking(false) + } + }, [playingTime, isSeeking, setIsSeeking]) + + useEffect(() => { + if (isReady && NodeStartTime && playerRef.current && !hasSeekedToStart) { + const startTimeInSeconds = videoTimeToSeconds(NodeStartTime) + + playerRef.current.seekTo(startTimeInSeconds, 'seconds') + setPlayingTime(startTimeInSeconds) + setHasSeekedToStart(true) + } + }, [isReady, NodeStartTime, setPlayingTime, hasSeekedToStart]) + + const togglePlay = () => { + setIsPlaying(!isPlaying) + } + + const handlePlay = () => { + setIsPlaying(true) + } + + const handlePause = () => { + setIsPlaying(false) + } + + const handleError = () => { + setHasError(true) + setStatus('error') + } + + const edges = useMemo(() => { + const edgesFiltered = dataInitial?.links.filter((link) => link?.start) || [] + + const sortedEdges = edgesFiltered.slice().sort((a, b) => (a?.start as number) - (b?.start as number)) + + return sortedEdges + }, [dataInitial]) + + const handleProgress = (progress: { playedSeconds: number }) => { + if (!isSeeking) { + const currentTime = progress.playedSeconds + + setPlayingTime(currentTime) + + const edge = findCurrentEdge(edges, currentTime) + + if (edge) { + setActiveEdge(edge) + } + + // find playing link and set it to state + } + } + + const handleReady = () => { + if (playerRef.current) { + setStatus('ready') + + const videoDuration = playerRef.current.getDuration() + + setDuration(videoDuration) + + if (NodeStartTime && !hasSeekedToStart) { + const startTimeInSeconds = videoTimeToSeconds(NodeStartTime) + + playerRef.current.seekTo(startTimeInSeconds, 'seconds') + setPlayingTime(startTimeInSeconds) + setHasSeekedToStart(true) + } + } + } + + const handleFullScreenChange = () => { + setIsFullScreen(!!document.fullscreenElement) + document.removeEventListener('fullscreenchange', handleFullScreenChange) + } + + useEffect(() => () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange) + }) + + useEffect(() => { + const handleMouseMove = (event: MouseEvent) => { + if (isFullScreen) { + const windowHeight = window.screen.height + const mousePositionY = event.clientY + const distanceFromBottom = windowHeight - mousePositionY + const threshold = 50 + + setIsMouseNearBottom(distanceFromBottom <= threshold) + } + } + + document.addEventListener('mousemove', handleMouseMove) + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + } + }, [isFullScreen, isMouseNearBottom]) + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (isFullScreen && event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + } else if (isFocused && event.key === ' ') { + event.preventDefault() + togglePlay() + } + } + + document.addEventListener('fullscreenchange', handleFullScreenChange) + document.addEventListener('keydown', handleKeyDown) + + return () => { + document.removeEventListener('fullscreenchange', handleFullScreenChange) + document.removeEventListener('keydown', handleKeyDown) + } + }) + + const handlePlayerClick = () => { + togglePlay() + } + + return mediaUrl ? ( + setIsFocused(false)} onFocus={() => setIsFocused(true)} tabIndex={0}> + + + + + setStatus('buffering')} + onBufferEnd={() => setStatus('ready')} + onError={handleError} + onPause={handlePause} + onPlay={handlePlay} + onProgress={handleProgress} + onReady={handleReady} + playing={isPlaying} + url={mediaUrl || ''} + volume={volume} + width="100%" + /> + + {status === 'error' ? ( + Error happened, please try later + ) : null} + + ) : null +} + +const Wrapper = styled(Flex)` + border-bottom: 1px solid rgba(0, 0, 0, 0.25); + background: rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; + height: auto; + &:focus { + outline: none; + } +` + +const Cover = styled(Flex)` + position: absolute; + top: ${(props) => (props.isFullScreen ? '38%' : '18%')}; + left: 50%; + transform: translateX(-50%); + z-index: -1; +` + +const ErrorWrapper = styled(Flex)` + height: 60px; + padding: 12px 16px; + color: ${colors.primaryRed}; +` + +const PlayerWrapper = styled.div<{ isFullScreen: boolean }>` + margin: ${(props) => (props.isFullScreen ? '80px auto' : '0')}; + width: 100%; + cursor: pointer; +` + +export const MediaPlayer = memo(MediaPlayerComponent) diff --git a/src/components/mindset/components/PlayerContols/CanvasProgressbar/index.tsx b/src/components/mindset/components/PlayerContols/CanvasProgressbar/index.tsx new file mode 100644 index 000000000..fd4200e2a --- /dev/null +++ b/src/components/mindset/components/PlayerContols/CanvasProgressbar/index.tsx @@ -0,0 +1,86 @@ +import { Html, OrbitControls } from '@react-three/drei' +import { Canvas, useThree } from '@react-three/fiber' +import { useEffect, useState } from 'react' +import { Tooltip } from '~/components/common/ToolTip' +import { nodesWithTimestamp } from '~/components/mindset/data' +import { Marker } from '../../Marker' + +type Props = { + duration: number + progress: number +} + +const FullWidthZoom = ({ progressBarWidth }: { progressBarWidth: number }) => { + const { size, camera } = useThree() + const [minZoom, setMinZoom] = useState(camera.zoom) + + useEffect(() => { + // Calculate the minimum zoom based on the progress bar width and viewport size + const calculatedZoom = size.width / progressBarWidth + + setMinZoom(calculatedZoom) + camera.zoom = calculatedZoom + camera.updateProjectionMatrix() + }, [size, camera, progressBarWidth]) + + return +} + +export const ProgressBarCanvas = ({ duration, progress }: Props) => { + const progressBarWidth = 100 // Width of the progress bar in world units + + return ( + + + + + {/* Full Width Zoom Calculation */} + + + {/* Progress Bar */} + + {/* Full progress bar width */} + + + + {/* Progress */} + + + + + + {/* Markers */} + {nodesWithTimestamp.map((node) => { + const position = ((node.start || 0) / duration) * progressBarWidth - progressBarWidth / 2 + const type = node?.node_type || '' + + return ( + + {/* Marker size */} + + {/* Tooltip */} + +
+ + + +
+ +
+ ) + })} +
+ ) +} diff --git a/src/components/mindset/components/PlayerContols/ProgressBar/index.tsx b/src/components/mindset/components/PlayerContols/ProgressBar/index.tsx new file mode 100644 index 000000000..3d4ef0ab7 --- /dev/null +++ b/src/components/mindset/components/PlayerContols/ProgressBar/index.tsx @@ -0,0 +1,65 @@ +import { LinearProgress } from '@mui/material' +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { Tooltip } from '~/components/common/ToolTip' +import { colors } from '~/utils' +import { Marker } from '../../Marker' +import { NodeExtended } from '~/types' + +type Props = { + duration: number + progress: number + markers: NodeExtended[] +} + +export const ProgressBar = ({ duration, progress, markers }: Props) => { + console.log('s') + + return ( + + + {markers.map((node) => { + const position = ((node?.start || 0) / duration) * 100 + const type = node?.node_type || '' + + return ( + + + + + + ) + })} + + ) +} + +const Progress = styled(LinearProgress)` + && { + height: 2px; + background-color: ${colors.white}; + color: blue; + flex-grow: 1; + + .MuiLinearProgress-bar { + background: ${colors.GRAY6}; + } + } +` + +const ProgressWrapper = styled(Flex)` + position: relative; + flex: 1 1 100%; +` + +const MarkerWrapper = styled.div` + position: absolute; + top: -4px; /* Adjust as needed to center above the progress bar */ + width: 8px; + height: 8px; + background-color: ${colors.white}; + border-radius: 50%; + transform: translateX(-50%); /* Center the marker horizontally */ + transform: translateX(-50%) translateY(-50%); + top: 50%; +` diff --git a/src/components/mindset/components/PlayerContols/index.tsx b/src/components/mindset/components/PlayerContols/index.tsx new file mode 100644 index 000000000..060f72367 --- /dev/null +++ b/src/components/mindset/components/PlayerContols/index.tsx @@ -0,0 +1,88 @@ +import { IconButton } from '@mui/material' +import styled from 'styled-components' +import ChevronLeftIcon from '~/components/Icons/ChevronLeftIcon' +import ChevronRightIcon from '~/components/Icons/ChevronRightIcon.js' +import PauseIcon from '~/components/Icons/PauseIcon' +import PlayIcon from '~/components/Icons/PlayIcon' +import { Flex } from '~/components/common/Flex' +import { usePlayerStore } from '~/stores/usePlayerStore' +import { NodeExtended } from '~/types' +import { videoTimeToSeconds } from '~/utils' +import { colors } from '~/utils/colors' +import { ProgressBarCanvas } from './CanvasProgressbar' +import { ProgressBar } from './ProgressBar' + +type Props = { + markers: NodeExtended[] +} + +export const PlayerControl = ({ markers }: Props) => { + const { isPlaying, setIsPlaying, playingTime, playingNode, duration } = usePlayerStore((s) => s) + + const [start, end] = playingNode?.properties?.timestamp + ? (playingNode.properties.timestamp as string).split('-').map((time) => videoTimeToSeconds(time)) + : [0, duration] + + const startTime = ((playingTime - start) / (end - start)) * 100 + + const showPlayer = playingNode + + return showPlayer ? ( + + + + { + setIsPlaying(!isPlaying) + e.stopPropagation() + }} + size="small" + > + {isPlaying ? : } + + + + + {false && } + + ) : null +} + +const Wrapper = styled(Flex).attrs({ + direction: 'row', + align: 'center', + justify: 'space-between', +})` + padding: 20px; + margin: 20px; + background: ${colors.BG2}; + height: 96px; + border-radius: 8px; + box-sizing: border-box; +` + +const Controls = styled(Flex).attrs({ + direction: 'row', + align: 'center', + justify: 'flex-start', +})` + width: 142px; + height: 54px; + background: ${colors.BG1}; + border-radius: 40px; + margin-right: 54px; + color: ${colors.white}; + font-size: 20px; + padding: 12px; + justify-content: space-between; + box-sizing: border-box; +` + +const Action = styled(IconButton)` + && { + font-size: 36px; + padding: 2px; + overflow: hidden; + } +` diff --git a/src/components/mindset/components/Scene/Board/Node/Content/index.tsx b/src/components/mindset/components/Scene/Board/Node/Content/index.tsx new file mode 100644 index 000000000..319977691 --- /dev/null +++ b/src/components/mindset/components/Scene/Board/Node/Content/index.tsx @@ -0,0 +1,47 @@ +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import PlusIcon from '~/components/Icons/PlusIcon' +import { colors } from '~/utils' + +type Props = { + name: string + url?: string +} + +export const Content = ({ name, url }: Props) => ( + + {url && } +
{name}
+
+ +
+
+) + +const Wrapper = styled(Flex)` + position: relative; + padding: 14px; + padding-right: 28px; + flex: 1; + width: 100%; + box-sizing: border-box; + + .title { + margin-top: 8px; + } + + .image { + width: 32px; + height: 32px; + border-radius: 50%; + margin-bottom: 8px; + object-fit: cover; + } + + .action-btn { + top: 14px; + right: 14px; + color: ${colors.GRAY6}; + position: absolute; + } +` diff --git a/src/components/mindset/components/Scene/Board/Node/index.tsx b/src/components/mindset/components/Scene/Board/Node/index.tsx new file mode 100644 index 000000000..e07cda450 --- /dev/null +++ b/src/components/mindset/components/Scene/Board/Node/index.tsx @@ -0,0 +1,66 @@ +import { Html } from '@react-three/drei' +import { useThree } from '@react-three/fiber' +import { OrthographicCamera } from 'three' +import { Flex } from '~/components/common/Flex' +import { RoundedRectangle } from '../RoundedRectangle' +import { Content } from './Content' + +type Props = { + width: number + height: number + position: [number, number, number] + url: string + onButtonClick: () => void + name: string + type: string + color: string +} + +export const Node = ({ width, height, position, url, onButtonClick, name, type, color }: Props) => { + const { camera, size } = useThree() + + console.info(url, type) + + // Function to calculate the distance between the camera and the node + const getPixelSize = (worldSize: number, worldHeight: number) => { + const ortographicCamera = camera as OrthographicCamera + const visibleWidth = ortographicCamera.right - ortographicCamera.left + const visibleHeight = ortographicCamera.top - ortographicCamera.bottom + + return { + pixelWidth: (worldSize / visibleWidth) * size.width, + pixelHeight: (worldHeight / visibleHeight) * size.height, + } + } + + // Calculate pixel dimensions for the node + const { pixelWidth, pixelHeight } = getPixelSize(width, height) + + return ( + + {/* Background Rectangle */} + + + {/* Html */} + + onButtonClick()} + style={{ + fontSize: '20px', + color: 'white', + fontWeight: 600, + width: `${pixelWidth}px`, + height: `${pixelHeight}px`, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '8px', // Optional for rounded corners + pointerEvents: 'auto', // Allow interaction + }} + > + + + + + ) +} diff --git a/src/components/mindset/components/Scene/Board/RoundedImage/index.tsx b/src/components/mindset/components/Scene/Board/RoundedImage/index.tsx new file mode 100644 index 000000000..eb205feeb --- /dev/null +++ b/src/components/mindset/components/Scene/Board/RoundedImage/index.tsx @@ -0,0 +1,23 @@ +import { Image } from '@react-three/drei' + +type Props = { + position: [number, number, number] + scale: [number, number, number] + url: string +} + +export const RoundedImage = ({ position, scale, url }: Props) => ( + + {/* Circle mask */} + + {/* Adjust radius as needed */} + + + + {/* Image */} + + +) diff --git a/src/components/mindset/components/Scene/Board/RoundedRectangle/index.tsx b/src/components/mindset/components/Scene/Board/RoundedRectangle/index.tsx new file mode 100644 index 000000000..f7fba1f57 --- /dev/null +++ b/src/components/mindset/components/Scene/Board/RoundedRectangle/index.tsx @@ -0,0 +1,37 @@ +import { Shape } from 'three' + +type Props = { + width: number + height: number + radius: number + color: string +} + +const createRoundedRect = (width: number, height: number, radius: number) => { + const shape = new Shape() + + shape.moveTo(-width / 2 + radius, -height / 2) + shape.lineTo(width / 2 - radius, -height / 2) + shape.quadraticCurveTo(width / 2, -height / 2, width / 2, -height / 2 + radius) + shape.lineTo(width / 2, height / 2 - radius) + shape.quadraticCurveTo(width / 2, height / 2, width / 2 - radius, height / 2) + shape.lineTo(-width / 2 + radius, height / 2) + shape.quadraticCurveTo(-width / 2, height / 2, -width / 2, height / 2 - radius) + shape.lineTo(-width / 2, -height / 2 + radius) + shape.quadraticCurveTo(-width / 2, -height / 2, -width / 2 + radius, -height / 2) + + return shape +} + +export const RoundedRectangle = ({ width, height, radius, color }: Props) => { + console.log(width, height) + + const roundedRectShape = createRoundedRect(width, height, radius) + + return ( + + + + + ) +} diff --git a/src/components/mindset/components/Scene/Board/index.tsx b/src/components/mindset/components/Scene/Board/index.tsx new file mode 100644 index 000000000..849c1577a --- /dev/null +++ b/src/components/mindset/components/Scene/Board/index.tsx @@ -0,0 +1,112 @@ +import { useThree } from '@react-three/fiber' +import { useEffect, useMemo } from 'react' +import { edges, edgesMention, maxTimestamp, minTimestamp, nodes, normalizeTimestamp } from '~/components/mindset/data' +import { Node } from './Node' + +const totalDuration = 185 + +export const Board = () => { + const state = useThree() + + const { width } = state.viewport + const { camera } = state + + useEffect(() => { + const orthoCamera = camera as THREE.OrthographicCamera + + const handleWheel = (event: WheelEvent) => { + event.preventDefault() // Prevent default scrolling behavior + + if (event.ctrlKey) { + // Zoom the camera when ctrlKey is pressed + orthoCamera.zoom += event.deltaY * -0.01 // Adjust zoom level + orthoCamera.zoom = Math.max(0.5, Math.min(orthoCamera.zoom, 5)) // Clamp zoom + } else { + // Move the camera left/right when ctrlKey is NOT pressed + orthoCamera.position.x += event.deltaX * 0.01 // Horizontal movement + } + + orthoCamera.updateProjectionMatrix() // Update projection matrix + } + + // Add the event listener + window.addEventListener('wheel', handleWheel, { passive: false }) + + return () => { + // Cleanup event listener + window.removeEventListener('wheel', handleWheel) + } + }, [camera]) + + const rangeMin = 0 + const rangeMax = 97.52 * 10 + + const positions = useMemo( + () => + edges + .filter((edge) => edge?.properties?.start && edge?.properties?.end) + .map((edge) => { + const st: number = (edge?.properties?.start || 0) as number + const ed: number = (edge?.properties?.end || 0) as number + + return { + source: edge.source, + target: edge.target, + xStart: normalizeTimestamp(st, minTimestamp, maxTimestamp, rangeMin, rangeMax), + xEnd: normalizeTimestamp(ed, minTimestamp, maxTimestamp, rangeMin, rangeMax), + } + }), + [rangeMin, rangeMax], + ) + + return ( + <> + {nodes.map((node, i) => { + const hasTimeStamp = positions.some((p) => p.source === nodes[i].ref_id) + + const position = hasTimeStamp ? positions.find((p) => p.source === nodes[i].ref_id)?.xStart || 0 : i * 35 + + const y = hasTimeStamp ? 0 : 15 + + return true ? null : ( + + ) + })} + {edgesMention.map((e) => { + const node = nodes.find((i) => i.ref_id === e.source) + + const x = (e.start / totalDuration) * width + + const y = -5 + + return node ? ( + null} + position={[x, y, 0]} + type={node.node_type} + url="logo.png" + width={5} + /> + ) : null + })} + + {/* Radius: Half of viewport width */} + + + + ) +} diff --git a/src/components/mindset/components/Scene/index.tsx b/src/components/mindset/components/Scene/index.tsx new file mode 100644 index 000000000..6d5f5913d --- /dev/null +++ b/src/components/mindset/components/Scene/index.tsx @@ -0,0 +1,64 @@ +import { Canvas, useThree } from '@react-three/fiber' +import { useEffect, useState } from 'react' +import { Board } from './Board' + +export const Scene = () => { + const [cameraX, setCameraX] = useState(0) // State for horizontal camera movement + + const handleSliderChange = (event: React.ChangeEvent) => { + setCameraX(Number(event.target.value)) + } + + return ( +
+ {/* Slider for controlling camera X-axis */} +
+ +
+ + + + + + + + {/* Add Axis Helper */} + + +
+ ) +} + +// Custom camera component +const DynamicOrthographicCamera = ({ x }: { x: number }) => { + const { size, viewport, camera } = useThree() + const frustumSize = 50 + + const aspect = size.width / size.height + + console.log(aspect, size.width) + + // Dynamically calculate camera bounds + const left = (-frustumSize * aspect) / 2 + const right = (frustumSize * aspect) / 2 + const top = frustumSize / 2 + const bottom = -frustumSize / 2 + + useEffect(() => { + // Update camera bounds dynamically + const orthoCamera = camera as THREE.OrthographicCamera + + orthoCamera.left = left + orthoCamera.right = right + orthoCamera.top = top + orthoCamera.bottom = bottom + orthoCamera.position.x = x + orthoCamera.updateProjectionMatrix() + }, [camera, left, right, top, bottom, x]) + + useEffect(() => { + console.log('Viewport Width in World Units:', viewport.width) + }, [viewport.width]) + + return null +} diff --git a/src/components/mindset/components/Sidebar/Transcript/index.tsx b/src/components/mindset/components/Sidebar/Transcript/index.tsx new file mode 100644 index 000000000..84472cabc --- /dev/null +++ b/src/components/mindset/components/Sidebar/Transcript/index.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components' +import { Flex } from '~/components/common/Flex' +import { transcript } from '~/components/mindset/data/transcript' +import { usePlayerStore } from '~/stores/usePlayerStore' +import { colors } from '~/utils' + +export const Transcript = () => { + const data = transcript + + const { playingTime } = usePlayerStore((s) => s) + + return ( + + Transcript ({playingTime}) + + {data.map((tr) => ( + {tr.text} + ))} + + + ) +} + +const Wrapper = styled(Flex)` + .heading { + font-weight: 700; + font-size: 12px; + } + color: ${colors.white}; + background: ${colors.BG1}; + border-radius: 8px; + padding: 24px; + overflow: scroll; + flex: 1 1 100%; +` + +const TranscriptWrapper = styled(Flex)` + flex-wrap: wrap; + flex: 1 1 100%; +` diff --git a/src/components/mindset/components/Sidebar/index.tsx b/src/components/mindset/components/Sidebar/index.tsx new file mode 100644 index 000000000..06dc065ff --- /dev/null +++ b/src/components/mindset/components/Sidebar/index.tsx @@ -0,0 +1,62 @@ +import styled from 'styled-components' +import { MENU_WIDTH } from '~/components/App/SideBar' +import { Flex } from '~/components/common/Flex' +import { Text } from '~/components/common/Text' +import { MediaPlayer } from '~/components/mindset/components/MediaPlayer' +import { useMindsetStore } from '~/stores/useMindsetStore' +import { Transcript } from './Transcript' + +export const SideBar = () => { + const { selectedEpisodeLink, selectedEpisode } = useMindsetStore((s) => s) + + return ( + + + {selectedEpisode?.node_type && {selectedEpisode?.node_type}} + {selectedEpisode?.name && {selectedEpisode?.name}} + {selectedEpisodeLink && } + + + + ) +} + +const Wrapper = styled(Flex)(({ theme }) => ({ + position: 'relative', + display: 'flex', + padding: '20px', + background: 'transparent', + width: '100%', + + [theme.breakpoints.up('sm')]: { + width: MENU_WIDTH, + }, +})) + +const Summary = styled(Text)` + font-family: Inter; + font-size: 20px; + font-weight: Bold; + line-height: 24.2px; + overflow-wrap: break-word; + white-space: normal; + word-break: break-word; + margin-right: 10px; +` + +const EpisodeTitle = styled(Text)` + font-family: Inter; + margin-top: 20px; + font-size: 14px; + font-weight: 500; + line-height: 16.94px; +` + +const MediaWrapper = styled(Flex)(({ theme }) => ({ + width: '100%', + margin: '16px auto', + zIndex: 29, + [theme.breakpoints.up('sm')]: { + width: '390px', + }, +})) diff --git a/src/components/mindset/data/index.tsx b/src/components/mindset/data/index.tsx new file mode 100644 index 000000000..6ae9d1491 --- /dev/null +++ b/src/components/mindset/data/index.tsx @@ -0,0 +1,154 @@ +import { Link, NodeExtended } from '~/types' + +// const transcript = 'Entrepreneurship is being revolutionized by Blockchain and AI, as they open new frontiers for innovation and secure digital solutions.' + +export const nodes: NodeExtended[] = [ + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '1', node_type: 'Clip', name: 'Podcast' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '2', node_type: 'Topic', name: 'Bitcoin' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '3', node_type: 'Topic', name: 'Blockchain' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '4', node_type: 'Topic', name: 'Hard Money' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '5', node_type: 'Topic', name: 'Digital Currency' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '6', node_type: 'Topic', name: 'Government Control' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '7', node_type: 'Topic', name: 'Inflation' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '8', node_type: 'Topic', name: 'Public Network' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '9', node_type: 'Topic', name: 'Energy Consumption' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '10', node_type: 'Topic', name: 'Immutability' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '11', node_type: 'Topic', name: 'Scarcity' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '12', node_type: 'Topic', name: 'Decentralization' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '13', node_type: 'Topic', name: 'Investment Risks' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '14', node_type: 'Topic', name: 'Adoption' }, + { edge_count: 0, x: 0, y: 0, z: 0, label: 'label', ref_id: '15', node_type: 'Person', name: 'Satoshi Nakamoto' }, +] + +export const edges: Link[] = [ + // Bitcoin as a digital currency + { + ref_id: '1', + edge_type: 'Mentioned', + target: '1', // Clip + source: '2', // Bitcoin + start: 7.68, + end: 19.619, + }, + + // Bitcoin's hard money nature + { + ref_id: '2', + edge_type: 'Mentioned', + target: '1', // Clip + source: '4', // Hard Money + start: 28.019, + end: 38.04, + }, + + // Blockchain as a public ledger + { + ref_id: '3', + edge_type: 'Mentioned', + target: '1', // Clip + source: '3', // Blockchain + start: 50.82, + end: 56.52, + }, + + // Scarcity of Bitcoin + { + ref_id: '4', + edge_type: 'Mentioned', + target: '1', // Clip + source: '5', // Scarcity + start: 19.439, + end: 25.619, + }, + + // Government control contrasted with Bitcoin + { + ref_id: '5', + edge_type: 'Mentioned', + target: '1', // Clip + source: '6', // Government Control + start: 34.619, + end: 43.02, + }, + + // Energy consumption in Bitcoin mining + { + ref_id: '6', + edge_type: 'Mentioned', + target: '1', // Clip + source: '9', // Energy Consumption + start: 31.8, + end: 38.04, + }, + + // Immutability ensured by the blockchain + { + ref_id: '7', + edge_type: 'Mentioned', + target: '1', // Clip + source: '10', // Immutability + start: 56.52, + end: 60.48, + }, + + // Decentralization of Bitcoin's public network + { + ref_id: '8', + edge_type: 'Mentioned', + target: '1', // Clip + source: '12', // Decentralization + start: 90.72, + end: 97.52, + }, + + // Bitcoin investment risks + { + ref_id: '9', + edge_type: 'Mentioned', + target: '1', // Clip + source: '13', // Investment Risks + start: 126.44, + end: 133.48, + }, + + // Bitcoin adoption rates and potential + { + ref_id: '10', + edge_type: 'Mentioned', + target: '1', // Clip + source: '14', // Adoption + start: 122.44, + end: 130.48, + }, +] + +export const edgesMention: Array<{ source: string; start: number }> = edges + .filter((e) => e?.start) + .map((edge) => ({ source: edge.source, start: edge?.start as number })) + +export const nodesWithTimestamp: NodeExtended[] = (nodes || []) + .filter((node) => edgesMention.some((ed) => ed.source === node.ref_id)) + .map((node) => { + const edge = edgesMention.find((ed) => node.ref_id === ed.source) + + return { ...node, start: edge?.start || 0 } + }) + .filter((node) => node) + +export const data = { + nodes, + edges, +} + +const timestamps = edges.flatMap((e) => { + const { start, end } = e.properties ?? {} + + return start !== undefined && end !== undefined ? [start, end] : [0, 0] +}) + +export const minTimestamp = Math.min(...(timestamps as number[])) + +export const maxTimestamp = Math.max(...(timestamps as number[])) + +export const normalizeTimestamp = (value: number, min: number, max: number, rangeMin: number, rangeMax: number) => + rangeMin + ((value - min) / (max - min)) * (rangeMax - rangeMin) diff --git a/src/components/mindset/data/transcript/index.tsx b/src/components/mindset/data/transcript/index.tsx new file mode 100644 index 000000000..0e097e8c0 --- /dev/null +++ b/src/components/mindset/data/transcript/index.tsx @@ -0,0 +1,76 @@ +export const transcript = [ + { text: 'when money is easy to make Society', timestamp: 179 }, + { text: 'begins to break', timestamp: 2760 }, + { text: 'well one solution is Bitcoin', timestamp: 7680 }, + { text: 'back in 2008 I became fed up with', timestamp: 11219 }, + { text: 'government money with the corruption the', timestamp: 13620 }, + { text: 'manipulation so I created a digital', timestamp: 15599 }, + { text: 'currency digital money that can be sent', timestamp: 17760 }, + { text: 'directly from one person to another', timestamp: 19619 }, + { text: 'without any bank or government involved', timestamp: 21119 }, + { text: "and the best part it's hard money", timestamp: 23400 }, + { text: 'Bitcoin is very hard to make more of', timestamp: 28019 }, + { text: 'each new coin gets added to the supply', timestamp: 29939 }, + { text: 'only after a computer works very hard to', timestamp: 31800 }, + { text: "solve a math problem where there's no", timestamp: 34200 }, + { text: 'shortcut and solving it costs a lot of', timestamp: 35880 }, + { text: 'energy and time', timestamp: 38040 }, + { text: "okay but if it's on a computer can't I", timestamp: 40559 }, + { text: 'just copy and paste', timestamp: 43020 }, + { text: 'not with Bitcoin you', timestamp: 46559 }, + { text: 'have public record of every Bitcoin ever', timestamp: 48600 }, + { text: "created it's called the blockchain it's", timestamp: 50820 }, + { text: 'like a puzzle and each Bitcoin has its', timestamp: 54300 }, + { text: 'own unique shape and because everyone', timestamp: 56520 }, + { text: 'has a copy of the public record if', timestamp: 58260 }, + { text: 'someone tries to fake a Bitcoin', timestamp: 60480 }, + { text: "it won't fit the puzzle and will be", timestamp: 65099 }, + { text: 'rejected by the network before anyone', timestamp: 67140 }, + { text: 'can use it', timestamp: 69060 }, + { text: "that's why Bitcoin is so safe from", timestamp: 70860 }, + { text: 'criminals and the government you said', timestamp: 72960 }, + { text: 'criminals twice', timestamp: 75420 }, + { text: 'dollars which can be printed endlessly', timestamp: 77439 }, + { text: 'there will only ever be 21 million', timestamp: 79720 }, + { text: "Bitcoin it's almost impossible to", timestamp: 81720 }, + { text: 'inflate the only way to get it is to', timestamp: 83759 }, + { text: 'earn it or buy it from someone who has', timestamp: 85619 }, + { text: 'no offense but if you', timestamp: 89820 }, + { text: "if you coin couldn't you manipulate it", timestamp: 91320 }, + { text: "just like the government I don't control", timestamp: 92880 }, + { text: "it no one person does it's controlled by", timestamp: 94619 }, + { text: 'a public network of Bitcoin users that', timestamp: 97619 }, + { text: 'anyone can join so if someone wanted to', timestamp: 99720 }, + { text: "change something in bitcoin's code they", timestamp: 101939 }, + { text: 'would need to get the majority of the', timestamp: 103920 }, + { text: 'millions of Bitcoin users to agree to it', timestamp: 104659 }, + { text: "all the change doesn't happen and when", timestamp: 104752 }, + { text: 'has the majority of us agreed on', timestamp: 104860 }, + { text: 'anything', timestamp: 105360 }, + { text: "touche wow I didn't know made up money", timestamp: 105720 }, + { text: 'could make so much sense today millions', timestamp: 105960 }, + { text: 'of people send Bitcoin instantly and', timestamp: 106180 }, + { text: 'cheaply to each other around the world', timestamp: 106399 }, + { text: 'without any bank or government involved', timestamp: 106620 }, + { text: 'and because Bitcoin is hard money you', timestamp: 106859 }, + { text: "can't just print more of the more people", timestamp: 107360 }, + { text: 'who use it the more valuable it becomes', timestamp: 107960 }, + { text: "Emily if we take Lyle's Bitcoin we could", timestamp: 108640 }, + { text: 'be rich I could pay for college entirely', timestamp: 109040 }, + { text: 'you could Outsource your movie and pay', timestamp: 109440 }, + { text: 'to have your name in the credits slow', timestamp: 109840 }, + { text: "down I'm not saying that Bitcoin will", timestamp: 110240 }, + { text: 'make you rich quick or even at all right', timestamp: 110640 }, + { text: 'now only about one percent of the world', timestamp: 110780 }, + { text: "owns Bitcoin and because it's still", timestamp: 111280 }, + { text: 'being adopted some days it goes up and', timestamp: 111580 }, + { text: 'even down also Investments or', timestamp: 111820 }, + { text: 'potentially lucrative can be risky', timestamp: 112380 }, + { text: 'oh I think I get it good money should be', timestamp: 112879 }, + { text: 'easy to use but hard to create and', timestamp: 113459 }, + { text: "because Bitcoin is digital it's fast and", timestamp: 113800 }, + { text: 'cheap to use and the blockchain makes it', timestamp: 113900 }, + { text: 'hard to inflate thanks Mr Satoshi', timestamp: 114379 }, + { text: 'what Mr Satoshi', timestamp: 114819 }, + { text: 'gone or was he Aaron even here', timestamp: 115340 }, +] diff --git a/src/components/mindset/index.tsx b/src/components/mindset/index.tsx new file mode 100644 index 000000000..60a208275 --- /dev/null +++ b/src/components/mindset/index.tsx @@ -0,0 +1,175 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Socket } from 'socket.io-client' +import { Flex } from '~/components/common/Flex' +import { Universe } from '~/components/Universe' +import { useSocket } from '~/hooks/useSockets' +import { fetchNodeEdges } from '~/network/fetchGraphData' +import { getNode } from '~/network/fetchSourcesData' +import { useDataStore } from '~/stores/useDataStore' +import { useMindsetStore } from '~/stores/useMindsetStore' +import { usePlayerStore } from '~/stores/usePlayerStore' +import { FetchDataResponse, NodeExtended } from '~/types' +import { Header } from './components/Header' +import { LandingPage } from './components/LandingPage' +import { PlayerControl } from './components/PlayerContols' +import { Scene } from './components/Scene' +import { SideBar } from './components/Sidebar' + +export const MindSet = () => { + const { addNewNode, isFetching, runningProjectId, dataInitial } = useDataStore((s) => s) + const [showTwoD, setShowTwoD] = useState(false) + const { selectedEpisodeId, setSelectedEpisode } = useMindsetStore((s) => s) + const socket: Socket | undefined = useSocket() + + const queueRef = useRef(null) + const timerRef = useRef(null) + + const { setPlayingNode } = usePlayerStore((s) => s) + + const handleNewNodeCreated = useCallback( + (data: FetchDataResponse) => { + if (isFetching) { + return + } + + if (!queueRef.current) { + queueRef.current = { nodes: [], edges: [] } + } + + if (data.edges) { + queueRef.current.edges.push(...data.edges) + } + + if (data.nodes) { + queueRef.current.nodes.push(...data.nodes) + } + + if (timerRef.current) { + clearTimeout(timerRef.current) + } + + timerRef.current = setTimeout(() => { + // Combine all queued data into a single update + + if (queueRef.current) { + const { nodes: newNodes, edges: newEdges } = queueRef.current + const batchedData = { nodes: newNodes, edges: newEdges } + + queueRef.current = { nodes: [], edges: [] } + addNewNode(batchedData) + } + }, 3000) // Adjust delay as necessary + }, + [addNewNode, isFetching], + ) + + const handleNodeUpdated = useCallback((node: NodeExtended) => { + console.log(node, 'uuuuuupdate') + }, []) + + useEffect(() => { + const init = async () => { + try { + const data = await fetchNodeEdges(selectedEpisodeId, 0, 50) + + if (data) { + data.nodes = data?.nodes.map((i) => ({ ...i, node_type: 'Topic' })) + handleNewNodeCreated(data) + } + } catch (error) { + console.error(error) + } + } + + if (selectedEpisodeId) { + init() + } + }, [selectedEpisodeId, handleNewNodeCreated]) + + useEffect(() => { + const init = async () => { + try { + const data = await getNode(selectedEpisodeId) + + if (data) { + setPlayingNode(data) + setSelectedEpisode(data) + } + } catch (error) { + console.error(error) + } + } + + if (selectedEpisodeId) { + init() + } + }, [selectedEpisodeId, setPlayingNode, setSelectedEpisode]) + + useEffect(() => { + if (socket) { + socket.on('new_node_created', handleNewNodeCreated) + socket.on('node_updated', handleNodeUpdated) + } + + return () => { + if (socket) { + socket.off() + } + } + }, [socket, handleNodeUpdated, handleNewNodeCreated]) + + useEffect(() => { + if (runningProjectId) { + try { + socket?.emit('update_project_id', { id: runningProjectId }) + } catch (error) { + console.error(error) + } + } + }, [runningProjectId, socket]) + + const markers = useMemo(() => { + if (dataInitial) { + const edgesMention: Array<{ source: string; start: number }> = dataInitial.links + .filter((e) => e?.start) + .map((edge) => ({ source: edge.source, start: edge?.start as number })) + + const nodesWithTimestamps = dataInitial.nodes + .filter((node) => dataInitial.links.some((ed) => ed.source === node.ref_id)) + .map((node) => { + const edge = edgesMention.find((ed) => node.ref_id === ed.source) + + return { ...node, start: edge?.start || 0 } + }) + .filter((node) => node) + + return nodesWithTimestamps + } + + return [] + }, [dataInitial]) + + return ( + + {selectedEpisodeId ? ( + <> + + setShowTwoD(!showTwoD)}> +
+ + + + + + {showTwoD ? : } + + + + + + ) : ( + + )} + + ) +} diff --git a/src/network/fetchGraphData/index.ts b/src/network/fetchGraphData/index.ts index f9c763384..86a1187ed 100644 --- a/src/network/fetchGraphData/index.ts +++ b/src/network/fetchGraphData/index.ts @@ -52,10 +52,10 @@ const fetchNodes = async ( return fetchWithLSAT() } -export const fetchNodeEdges = async (refId: string, skip: number): Promise => { +export const fetchNodeEdges = async (refId: string, skip: number, limit = 5): Promise => { try { const response = await api.get( - `/prediction/graph/edges/${refId}?skip=${skip}&limit=5&sort_by="edge_count&include_properties=true&includeContent=true"`, + `/prediction/graph/edges/${refId}?skip=${skip}&limit=${limit}&sort_by="edge_count&include_properties=true&includeContent=true&depth=1"`, ) return response diff --git a/src/network/fetchSourcesData/index.ts b/src/network/fetchSourcesData/index.ts index 926f7ea55..9f083aa47 100644 --- a/src/network/fetchSourcesData/index.ts +++ b/src/network/fetchSourcesData/index.ts @@ -4,6 +4,7 @@ import { FetchEdgeTypesResponse, FetchRadarResponse, FetchTopicResponse, + NodeExtended, NodeRequest, RadarRequest, SubmitErrRes, @@ -314,6 +315,12 @@ export const deleteNode = async (id: string) => { return response } +export const getNode = async (id: string) => { + const response = await api.get(`/node/${id}`) + + return response +} + export const getPriceData = async (endpoint: string) => { const response = await api.get(`/getprice?endpoint=${endpoint}&method=post`) diff --git a/src/stores/useDataStore/index.ts b/src/stores/useDataStore/index.ts index 157d30449..866291d1c 100644 --- a/src/stores/useDataStore/index.ts +++ b/src/stores/useDataStore/index.ts @@ -1,15 +1,13 @@ -// @ts-nocheck -// @ts-ignore - import { isEqual } from 'lodash' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { fetchGraphData } from '~/network/fetchGraphData' -import { FilterParams, GraphData, Link, NodeExtended, NodeType, Sources, Trending, TStats } from '~/types' +import { FetchDataResponse, FilterParams, Link, NodeExtended, NodeType, Sources, Trending, TStats } from '~/types' import { useAiSummaryStore } from '../useAiSummaryStore' import { useAppStore } from '../useAppStore' +import { useUserStore } from '../useUserStore' -const deduplicateByRefId = (items) => { +const deduplicateByRefId = (items: Array) => { const uniqueMap = new Map() items.forEach((item) => { @@ -61,7 +59,6 @@ export type DataStore = { runningProjectMessages: string[] setTrendingTopics: (trendingTopics: Trending[]) => void - setDataNew: (data: GraphData) => void resetDataNew: () => void setStats: (stats: TStats) => void setSidebarFilter: (filter: string) => void @@ -114,14 +111,17 @@ const defaultData: Omit< | 'removeNode' | 'setAbortRequests' | 'nextPage' - | 'setDataNew' | 'resetDataNew' | 'setSeedQuestions' + | 'setRunningProjectId' + | 'setRunningProjectMessages' + | 'resetRunningProjectMessages' + | 'abortFetchData' + | 'resetGraph' + | 'resetData' > = { categoryFilter: null, dataInitial: null, - currentPage: 0, - itemsPerPage: 300, runningProjectMessages: [], filters: { skip: 0, @@ -149,6 +149,8 @@ const defaultData: Omit< dataNew: null, seedQuestions: null, runningProjectId: '', + hideNodeDetails: false, + nodeTypes: [], } let abortController: AbortController | null = null @@ -284,7 +286,7 @@ export const useDataStore = create()( sidebarFilterCounts, }) } catch (error) { - console.log(error) + console.error(error) if (error !== 'abort') { set({ isLoadingNew: false, isFetching: false }) @@ -298,13 +300,16 @@ export const useDataStore = create()( } }, resetGraph: () => { + const { setAbortRequests } = get() + const { setBudget } = useUserStore.getState() + set({ filters: defaultData.filters, dataInitial: null, dataNew: null, }) - get().fetchData() + get().fetchData(setBudget, setAbortRequests) }, resetData: () => { @@ -315,17 +320,19 @@ export const useDataStore = create()( }) }, - setPage: (page: number) => set({ currentPage: page }), nextPage: () => { - const { filters, fetchData } = get() + const { filters, fetchData, setAbortRequests } = get() + const { setBudget } = useUserStore.getState() set({ filters: { ...filters, skip: filters.skip + 1 } }) - fetchData() + fetchData(setBudget, setAbortRequests) }, resetDataNew: () => null, setFilters: (filters: Partial) => { + const { setBudget } = useUserStore.getState() + set((state) => ({ filters: { ...state.filters, ...filters, skip: 0 } })) - get().fetchData(get().setBudget, get().setAbortRequests) + get().fetchData(setBudget, get().setAbortRequests) }, setSidebarFilterCounts: (sidebarFilterCounts) => set({ sidebarFilterCounts }), setTrendingTopics: (trendingTopics) => set({ trendingTopics }), @@ -340,7 +347,7 @@ export const useDataStore = create()( setHideNodeDetails: (hideNodeDetails) => set({ hideNodeDetails }), setSeedQuestions: (questions) => set({ seedQuestions: questions }), updateNode: (updatedNode) => { - console.log(updatedNode) + console.info(updatedNode) }, addNewNode: (data) => { const { dataInitial: existingData, filters } = get() diff --git a/src/stores/useGraphStore/index.ts b/src/stores/useGraphStore/index.ts index d2c062aa7..67dae899e 100644 --- a/src/stores/useGraphStore/index.ts +++ b/src/stores/useGraphStore/index.ts @@ -80,6 +80,7 @@ export type GraphStore = { simulation: ForceSimulation | null simulationHelpers: SimulationHelpers isHovering: boolean + activeEdge: Link | null setDisableCameraRotation: (rotation: boolean) => void setScrollEventsDisabled: (rotation: boolean) => void @@ -88,6 +89,7 @@ export type GraphStore = { setGraphRadius: (graphRadius: number) => void setHoveredNode: (hoveredNode: NodeExtended | null) => void setSelectedNode: (selectedNode: NodeExtended | null) => void + setActiveEdge: (edge: Link | null) => void setCameraFocusTrigger: (_: boolean) => void setNearbyNodeIds: (_: string[]) => void setShowSelectionGraph: (_: boolean) => void @@ -104,6 +106,7 @@ const defaultData: Omit< | 'setDisableCameraRotation' | 'setHoveredNode' | 'setSelectedNode' + | 'setActiveEdge' | 'setCameraFocusTrigger' | 'setGraphRadius' | 'setGraphStyle' @@ -123,6 +126,7 @@ const defaultData: Omit< graphStyle: (localStorage.getItem('graphStyle') as GraphStyle) || 'sphere', hoveredNode: null, selectedNode: null, + activeEdge: null, cameraFocusTrigger: false, nearbyNodeIds: [], showSelectionGraph: false, @@ -144,6 +148,9 @@ export const useGraphStore = create()((set, get) => ({ setHoveredNode: (hoveredNode) => { set({ hoveredNode }) }, + setActiveEdge: (activeEdge) => { + set({ activeEdge }) + }, setSelectedNode: (selectedNode) => { const { selectedNode: stateSelectedNode, simulation } = get() @@ -203,7 +210,7 @@ export const useGraphStore = create()((set, get) => ({ simulationHelpers.simulationRestart() } catch (error) { - console.log(error) + console.error(error) // eslint-disable-next-line no-debugger } @@ -307,8 +314,6 @@ export const useGraphStore = create()((set, get) => ({ }, }, simulationCreate: (nodes, links) => { - console.log('created') - const structuredNodes = structuredClone(nodes) const structuredLinks = structuredClone(links) diff --git a/src/stores/useMindsetStore/index.ts b/src/stores/useMindsetStore/index.ts new file mode 100644 index 000000000..2b936cbfc --- /dev/null +++ b/src/stores/useMindsetStore/index.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' +import { NodeExtended } from '~/types' + +type MindsetStore = { + selectedEpisodeId: string + selectedEpisode: NodeExtended | null + selectedEpisodeLink: string + setSelectedEpisodeId: (id: string) => void + setSelectedEpisode: (node: NodeExtended) => void + setSelectedEpisodeLink: (link: string) => void +} + +const defaultData: Omit = { + selectedEpisodeId: '', + selectedEpisodeLink: '', + selectedEpisode: null, +} + +export const useMindsetStore = create((set) => ({ + ...defaultData, + setSelectedEpisodeId: (selectedEpisodeId) => set({ selectedEpisodeId }), + setSelectedEpisodeLink: (selectedEpisodeLink) => set({ selectedEpisodeLink }), + setSelectedEpisode: (selectedEpisode) => set({ selectedEpisode }), +})) diff --git a/src/stores/useTopicsStore/index.ts b/src/stores/useTopicsStore/index.ts index 04f22b13a..5120004d0 100644 --- a/src/stores/useTopicsStore/index.ts +++ b/src/stores/useTopicsStore/index.ts @@ -67,7 +67,7 @@ export const useTopicsStore = create((set, get) => ({ set({ loading: false }) } catch (error) { - console.log(error) + console.error(error) } }, setFilters: (filters: Partial) => set({ filters: { ...get().filters, page: 0, ...filters } }), diff --git a/src/testSphinxBridge/saveLsat.ts b/src/testSphinxBridge/saveLsat.ts index c8346a19c..ebb2e99b7 100644 --- a/src/testSphinxBridge/saveLsat.ts +++ b/src/testSphinxBridge/saveLsat.ts @@ -28,7 +28,7 @@ export async function saveLsat(invoice: string, macaroon: string, host: string) return null } catch (error) { - console.log(JSON.stringify(error)) + console.error(JSON.stringify(error)) return null } diff --git a/src/transformers/splitGraph.ts b/src/transformers/splitGraph.ts index 07eb941b6..620540edc 100644 --- a/src/transformers/splitGraph.ts +++ b/src/transformers/splitGraph.ts @@ -195,31 +195,6 @@ export const generateSplitGraphPositions = (nodes: NodeExtended[], links: Link[] return updated }) - // const links = generateLinksFromNodeData(updatedNodes, true, true) - - // do links - const updatedLinks = links.map((l: Link) => { - const sourceNode = updatedNodes.find((f) => f.ref_id === l.sourceRef) - const targetNode = updatedNodes.find((f) => f.ref_id === l.targetRef) - let onlyVisibleOnSelect = false - - if (sourceNode?.node_type === 'Guest' || sourceNode?.node_type === 'Topic') { - onlyVisibleOnSelect = true - } - - const sourcePosition = new Vector3(sourceNode?.x || 0, sourceNode?.y || 0, sourceNode?.z || 0) - const targetPosition = new Vector3(targetNode?.x || 0, targetNode?.y || 0, targetNode?.z || 0) - - return { - ...l, - onlyVisibleOnSelect, - sourcePosition, - targetPosition, - } - }) - - console.log(updatedLinks) - // sort back to weighted sort updatedNodes.sort((a, b) => (b.weight || 0) - (a.weight || 0)) diff --git a/src/types/index.ts b/src/types/index.ts index 152948948..01d126700 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,10 +15,12 @@ export type FilterParams = { skip: number limit: number depth: string + includeContent: string sort_by: string top_node_count: string include_properties: string - node_type: string[] | string + node_type: string[] + search_method: string free?: string word?: string // Add other optional filter properties as needed } @@ -141,6 +143,8 @@ export type NodeExtended = Node & { x?: number y?: number z?: number + start?: number + end?: number longitude?: number latitude?: number coordinates?: Coordinates @@ -150,6 +154,8 @@ export type NodeExtended = Node & { export type Link = { index?: T extends string ? never : number + start?: number + end?: number source: T target: T color?: number @@ -160,6 +166,7 @@ export type Link = { sourcePosition?: Vector3 targetPosition?: Vector3 onlyVisibleOnSelect?: boolean + properties?: { [key: string]: unknown } } export type GraphData = { diff --git a/src/utils/colors/index.tsx b/src/utils/colors/index.tsx index 47f7ce070..38cbea411 100644 --- a/src/utils/colors/index.tsx +++ b/src/utils/colors/index.tsx @@ -108,6 +108,9 @@ export const colors = { createTestButton: 'rgb(178, 255, 102)', MESSAGE_BG: 'rgba(22, 22, 29, 0.89)', MESSAGE_BG_HOVER: 'rgba(35, 37, 47, 0.3)', + DIVIDER_4: 'rgba(46, 55, 67, 1)', + INPUT_BG: 'rgba(255, 255, 255, 0.05)', + INPUT_PLACEHOLDER: 'rgba(255, 255, 255, 0.5)', } as const export type ColorName = keyof typeof colors diff --git a/src/utils/relayHelper/index.ts b/src/utils/relayHelper/index.ts index f807afdac..597000a0a 100644 --- a/src/utils/relayHelper/index.ts +++ b/src/utils/relayHelper/index.ts @@ -27,10 +27,6 @@ export const saveSearchTerm = async () => { await executeIfProd(async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const res = await sphinx.enable(true) - - if (!res) { - console.log('Sphinx enable failed, means no pubkey and no budget (including budget of 0)') - } + await sphinx.enable(true) }) }