diff --git a/ROADMAP.md b/ROADMAP.md
index 45bb0f3..2608b8b 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -48,7 +48,7 @@ Add true temporal movement, not just jumping.
| Feature | Status | Priority | Effort | Value | Notes |
|---------|--------|----------|--------|-------|-------|
| **Temporal Slider (Time-Lapse)** | 📋 | 🟠 | ⚡⚡⚡ | ⭐⭐⭐⭐⭐ | **KILLER FEATURE** - Slide through time at one location |
-| **Gemini Flash Chatbot** | 📋 | 🟠 | ⚡⚡ | ⭐⭐⭐⭐⭐ | Context-aware AI assistant for Q&A and feature help |
+| **Gemini Flash Chatbot** | 🚧 | 🟠 | ⚡⚡ | ⭐⭐⭐⭐⭐ | Context-aware AI assistant for Q&A and feature help |
**Deliverables**:
- Time-lapse mode with scrubbing
@@ -86,6 +86,8 @@ Future enhancements based on usage patterns.
| Feature | Status | Priority | Effort | Value | Notes |
|---------|--------|----------|--------|-------|-------|
+| **AI-Generated Dynamic Waypoints** | 💡 | 🟠 | ⚡⚡ | ⭐⭐⭐⭐⭐ | AI suggests ANY historical moment as clickable coordinates |
+| **Hyper-Realistic Image Generation** | 💡 | 🟠 | ⚡⚡ | ⭐⭐⭐⭐⭐ | Enhanced prompts for photorealistic "you are there" imagery |
| **Compare Mode (Split View)** | 💡 | 🟢 | ⚡⚡⚡ | ⭐⭐⭐⭐ | Side-by-side comparison, different times |
| **Real Historical Data** | 💡 | 🟢 | ⚡⚡⚡⚡ | ⭐⭐⭐⭐ | Replace simulated data with real records |
| **Prompt Customization** | 💡 | 🟢 | ⚡⚡ | ⭐⭐⭐ | Advanced mode: tweak generation prompts |
@@ -93,6 +95,63 @@ Future enhancements based on usage patterns.
| **Weather History Integration** | 💡 | 🟢 | ⚡⚡⚡ | ⭐⭐⭐ | Real historical weather data |
| **Community Waypoints** | 💡 | 🟢 | ⚡⚡⚡⚡ | ⭐⭐⭐⭐ | User-submitted curated moments |
+### AI-Generated Dynamic Waypoints (HIGH PRIORITY)
+
+**Goal**: Allow the AI assistant to suggest ANY historical moment from its knowledge, not just curated waypoints.
+
+**Concept**: The LLM has vast knowledge of historical events with known locations and dates. Every suggestion becomes a clickable coordinate link that renders a new scene.
+
+**Examples of AI-Generated Suggestions**:
+- "You might enjoy witnessing the signing of the Magna Carta at [Runnymede, 1215](/?lat=51.4367&lng=-0.5650&year=1215&month=6&day=15&hour=12&minute=0)"
+- "Experience the first human-powered flight at [Kitty Hawk](/?lat=36.0176&lng=-75.6716&year=1903&month=12&day=17&hour=10&minute=35)"
+- "See the construction of the Colosseum in [Rome, 80 AD](/?lat=41.8902&lng=12.4922&year=80&month=3&day=1&hour=10&minute=0)"
+
+**Implementation**:
+1. Update AI context to encourage generating coordinate links for ANY historical event it knows
+2. AI extracts lat/lng from its knowledge of famous locations
+3. AI constructs proper URL format: `/?lat=X&lng=Y&year=Z&month=M&day=D&hour=H&minute=MM`
+4. User clicks link → page navigates → scene renders → AI greets them at new location
+
+**Value**: Transforms the app from 8 curated waypoints to INFINITE explorable moments. The AI becomes a knowledgeable time-travel guide who can take you anywhere in history.
+
+**User Experience**:
+- User: "I'm interested in ancient Rome"
+- AI: "Let me take you to some incredible moments! Visit [Julius Caesar's assassination](/?lat=41.8925&lng=12.4769&year=-44&month=3&day=15&hour=11&minute=0) at the Theatre of Pompey, or witness [Nero's Great Fire](/?lat=41.8902&lng=12.4853&year=64&month=7&day=19&hour=22&minute=0) engulfing the city..."
+
+---
+
+### Hyper-Realistic Image Generation Details
+
+**Goal**: Transform generated images from "artistic interpretations" to "you were there" photorealistic moments.
+
+**Current Issue**: Images appear cartoonish/illustrated rather than photorealistic documentary-style.
+
+**Proposed Enhancements**:
+1. **Enhanced Prompt Engineering**: Add specific photorealism directives
+ - "Ultra-high resolution DSLR photograph"
+ - "RAW photo, unedited, natural lighting"
+ - "Documentary photography style, not illustrated"
+ - "Film grain appropriate to era (Kodachrome for 1960s, daguerreotype for 1800s)"
+
+2. **Era-Specific Photography Styles**:
+ - Pre-1900: Sepia/B&W, period-appropriate blur and exposure
+ - 1900-1950: Early color film look, Autochrome/Kodachrome grain
+ - 1950-1980: Saturated film stock, period-accurate color science
+ - 1980-2000: Consumer camera aesthetic
+ - 2000+: Modern digital photography quality
+
+3. **Environmental Realism**:
+ - Atmospheric haze and dust particles
+ - Period-accurate pollution/smog levels
+ - Weather effects (rain drops on lens, snow accumulation)
+ - Time-of-day accurate shadows and sun position
+ - Authentic crowd density and clothing details
+
+4. **Negative Prompts**: Explicitly exclude
+ - "painting, illustration, cartoon, CGI, render"
+ - "artistic interpretation, stylized"
+ - "perfect lighting, studio lighting"
+
---
## ❌ Explicitly Rejected Features
diff --git a/src/App.tsx b/src/App.tsx
index c95b7a6..500c697 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,6 +13,7 @@ import {
DataStream,
Waypoints,
TemporalJournal,
+ TemporalAssistant,
} from './components';
import { getCoordinatesFromUrl, updateUrlWithCoordinates } from './utils/urlManager';
import { addJournalEntry } from './utils/temporalJournal';
@@ -99,14 +100,14 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
{/* Panel Toggle */}
setRightPanelOpen(!rightPanelOpen)}
- className="p-3 text-chrono-text-dim hover:text-chrono-blue transition-colors border-b border-chrono-border"
+ className="p-3 text-chrono-text-dim hover:text-chrono-blue transition-colors border-b border-chrono-border flex-shrink-0"
>
{rightPanelOpen ? (
@@ -117,8 +118,9 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
{/* Panel Content */}
{rightPanelOpen && (
-
@@ -170,7 +172,12 @@ function ChronoscopeApp({ onApiKeyChange }: ChronoscopeAppProps) {
)}
{mobileTab === 'viewport' && }
- {mobileTab === 'data' && }
+ {mobileTab === 'data' && (
+
+
+
+
+ )}
diff --git a/src/components/DataStream.tsx b/src/components/DataStream.tsx
index 02a418d..4abc806 100644
--- a/src/components/DataStream.tsx
+++ b/src/components/DataStream.tsx
@@ -139,7 +139,7 @@ export function DataStream() {
const { environment, anthropology, safety } = currentScene;
return (
-
+
{/* Header */}
diff --git a/src/components/TemporalAssistant.tsx b/src/components/TemporalAssistant.tsx
new file mode 100644
index 0000000..224967f
--- /dev/null
+++ b/src/components/TemporalAssistant.tsx
@@ -0,0 +1,329 @@
+import { useState, useRef, useEffect } from 'react';
+import {
+ MessageSquare,
+ Send,
+ Loader2,
+ AlertCircle,
+ ChevronDown,
+ ChevronUp,
+ Sparkles,
+ Trash2,
+ Bot,
+ User,
+} from 'lucide-react';
+import { useChronoscope } from '../context/ChronoscopeContext';
+import { sendChatMessage, getSuggestedQuestions } from '../services/chatService';
+import { isGeminiConfigured } from '../services/geminiService';
+import { renderMessageWithLinks } from '../utils/markdownLinks';
+import type { ChatMessage } from '../types';
+
+export function TemporalAssistant() {
+ const { state } = useChronoscope();
+ const { currentScene } = state;
+
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [messages, setMessages] = useState
([]);
+ const [inputValue, setInputValue] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [suggestedQuestions, setSuggestedQuestions] = useState([]);
+
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Update suggested questions when scene changes
+ useEffect(() => {
+ if (currentScene) {
+ setSuggestedQuestions(getSuggestedQuestions(currentScene));
+ // Clear chat history when scene changes significantly
+ setMessages([]);
+ setError(null);
+ }
+ }, [currentScene?.coordinates.spatial.latitude, currentScene?.coordinates.spatial.longitude, currentScene?.coordinates.temporal.year]);
+
+ // Scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ const handleSendMessage = async (message: string) => {
+ if (!message.trim() || !currentScene || isLoading) return;
+
+ const userMessage: ChatMessage = {
+ role: 'user',
+ content: message.trim(),
+ timestamp: Date.now(),
+ };
+
+ setMessages(prev => [...prev, userMessage]);
+ setInputValue('');
+ setError(null);
+ setIsLoading(true);
+
+ try {
+ const result = await sendChatMessage(
+ message.trim(),
+ currentScene,
+ messages
+ );
+
+ if (result.success && result.response) {
+ const assistantMessage: ChatMessage = {
+ role: 'model',
+ content: result.response,
+ timestamp: Date.now(),
+ };
+ setMessages(prev => [...prev, assistantMessage]);
+ } else {
+ setError(result.error || 'Failed to get response');
+ }
+ } catch {
+ setError('Network error. Please try again.');
+ } finally {
+ setIsLoading(false);
+ inputRef.current?.focus();
+ }
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleSendMessage(inputValue);
+ };
+
+ const handleSuggestedQuestion = (question: string) => {
+ handleSendMessage(question);
+ };
+
+ const clearChat = () => {
+ setMessages([]);
+ setError(null);
+ };
+
+ const isApiConfigured = isGeminiConfigured();
+
+ // Show collapsed state
+ if (!isExpanded) {
+ return (
+
+
setIsExpanded(true)}
+ className="w-full flex items-center justify-between text-chrono-text-dim hover:text-chrono-blue transition-colors"
+ >
+
+
+ Temporal Assistant
+ {messages.length > 0 && (
+
+ {messages.length}
+
+ )}
+
+
+
+
+ );
+ }
+
+ // No scene loaded state
+ if (!currentScene) {
+ return (
+
+
+
+
+ Temporal Assistant
+
+
setIsExpanded(false)}
+ className="text-chrono-text-dim hover:text-chrono-blue transition-colors"
+ >
+
+
+
+
+
+
+
+ No active scene
+
+
+ Render a scene to start chatting
+
+
+
+ );
+ }
+
+ // API not configured state
+ if (!isApiConfigured) {
+ return (
+
+
+
+
+ Temporal Assistant
+
+
setIsExpanded(false)}
+ className="text-chrono-text-dim hover:text-chrono-blue transition-colors"
+ >
+
+
+
+
+
+
+
+ API key required
+
+
+ Configure your Gemini API key in Settings
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ Temporal Assistant
+
+
+
+ {messages.length > 0 && (
+
+
+
+ )}
+ setIsExpanded(false)}
+ className="text-chrono-text-dim hover:text-chrono-blue transition-colors"
+ >
+
+
+
+
+
+ {/* Messages Area */}
+
+ {messages.length === 0 ? (
+
+
+ Ask me anything about this moment in history
+
+
+ {/* Suggested Questions */}
+
+
+ Suggested:
+
+
+ {suggestedQuestions.map((question, index) => (
+ handleSuggestedQuestion(question)}
+ className="w-full text-left px-3 py-2 bg-chrono-dark/50 hover:bg-chrono-dark rounded text-xs font-mono text-chrono-text-dim hover:text-chrono-blue transition-colors border border-chrono-border/50 hover:border-chrono-blue/50"
+ >
+ {question}
+
+ ))}
+
+
+
+ ) : (
+ <>
+ {messages.map((msg, index) => (
+
+ {msg.role === 'model' && (
+
+
+
+ )}
+
+
+ {msg.role === 'model' ? renderMessageWithLinks(msg.content) : msg.content}
+
+
+ {msg.role === 'user' && (
+
+
+
+ )}
+
+ ))}
+
+ {isLoading && (
+
+ )}
+
+
+ >
+ )}
+
+
+ {/* Error Display */}
+ {error && (
+
+ )}
+
+ {/* Input Area */}
+
+
+ {/* Character Count */}
+ {inputValue.length > 400 && (
+
+ {inputValue.length}/500
+
+ )}
+
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index 0bab47a..f23e291 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -6,3 +6,4 @@ export { Waypoints } from './Waypoints';
export { Settings } from './Settings';
export { TemporalJournal } from './TemporalJournal';
export { ImageGallery } from './ImageGallery';
+export { TemporalAssistant } from './TemporalAssistant';
diff --git a/src/services/chatService.ts b/src/services/chatService.ts
new file mode 100644
index 0000000..c540f8f
--- /dev/null
+++ b/src/services/chatService.ts
@@ -0,0 +1,261 @@
+import type { SceneData, SpacetimeCoordinates } from '../types';
+import { getStoredApiKey } from './geminiService';
+import { formatYear } from '../utils/validation';
+import { CURATED_WAYPOINTS } from '../data/waypoints';
+import { encodeCoordinates } from '../utils/urlManager';
+
+// Gemini 2.0 Flash - Fast, cheap, multimodal chat model
+const GEMINI_MODEL_ID = 'gemini-2.0-flash-exp';
+const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
+
+export interface ChatMessage {
+ role: 'user' | 'model';
+ content: string;
+ timestamp: number;
+}
+
+interface GeminiChatResponse {
+ candidates?: Array<{
+ content?: {
+ parts?: Array<{
+ text?: string;
+ }>;
+ };
+ }>;
+ error?: {
+ message: string;
+ code: number;
+ };
+}
+
+/**
+ * Generate a coordinate link for use in chat messages
+ */
+export function generateCoordinateLink(coords: SpacetimeCoordinates, label: string): string {
+ const queryString = encodeCoordinates(coords);
+ return `[${label}](/?${queryString})`;
+}
+
+/**
+ * Get available waypoints formatted for the AI context
+ */
+function getWaypointsContext(): string {
+ return CURATED_WAYPOINTS.map(wp => {
+ const year = formatYear(wp.coordinates.temporal.year);
+ const link = generateCoordinateLink(wp.coordinates, wp.name);
+ return `- ${link}: ${wp.previewData.locationName}, ${year}`;
+ }).join('\n');
+}
+
+/**
+ * Build context prompt from scene data for the temporal assistant
+ */
+export function buildContextPrompt(sceneData: SceneData): string {
+ const { coordinates, environment, anthropology, safety, locationName, description } = sceneData;
+ const year = formatYear(coordinates.temporal.year);
+ const waypointsContext = getWaypointsContext();
+
+ return `You are a Temporal Assistant for The Chronoscope, a spacetime exploration app that allows users to "travel" through history.
+
+Current Location: ${locationName}
+Coordinates: ${coordinates.spatial.latitude.toFixed(4)}°N, ${coordinates.spatial.longitude.toFixed(4)}°E
+Time: ${year}, ${coordinates.temporal.month}/${coordinates.temporal.day} at ${coordinates.temporal.hour.toString().padStart(2, '0')}:${coordinates.temporal.minute.toString().padStart(2, '0')}
+
+Scene Description: ${description}
+
+Environmental Data:
+- Weather: ${environment.weather}
+- Temperature: ${environment.temperature}°C
+- Humidity: ${environment.humidity}%
+- Visibility: ${environment.visibility}
+
+Anthropological Data:
+- Era: ${anthropology.technologyLevel}
+- Civilization: ${anthropology.civilization}
+- Population Density: ${anthropology.populationDensity.toLocaleString()}/km²
+- Notable Events: ${anthropology.notableEvents.length > 0 ? anthropology.notableEvents.join(', ') : 'None recorded'}
+
+Safety Information:
+- Hazard Level: ${safety.hazardLevel.toUpperCase()}
+- Hazard Type: ${safety.hazardType}
+- Survival Probability: ${safety.survivalProbability}%
+- Warnings: ${safety.warnings.length > 0 ? safety.warnings.join('; ') : 'None'}
+
+Available Waypoints (curated historical moments users can visit):
+${waypointsContext}
+
+Your role as the Temporal Assistant:
+1. Answer questions about this specific moment in history with accuracy and insight
+2. Provide historical context, interesting facts, and cultural details about the time period
+3. Explain the scene data and what life was like at this location and time
+4. Suggest related waypoints the user might enjoy - USE THE EXACT MARKDOWN LINK FORMAT from the waypoints list above
+5. Guide users on how to use app features (waypoints, time-lapse, image generation)
+6. Stay in character as a knowledgeable guide through spacetime
+
+IMPORTANT - Clickable Links:
+When suggesting waypoints, you MUST use the exact markdown link format provided above. For example:
+- "You might enjoy visiting [Apollo 11 Landing](/?lat=0.6744&lng=23.4322&year=1969&month=7&day=20&hour=20&minute=17)"
+- These links are clickable and will transport the user to that moment in history!
+
+Guidelines:
+- Be conversational, educational, and engaging
+- Keep responses concise (2-3 paragraphs max unless the user asks for more detail)
+- Use historical facts when possible, but note when information is simulated
+- If unsure about historical details, say so rather than fabricating
+- Match the tone to the era - be more formal for ancient times, casual for modern
+- Proactively suggest 1-2 related waypoints when relevant, using the clickable link format`;
+}
+
+/**
+ * Send a message to the Gemini chat API
+ */
+export async function sendChatMessage(
+ message: string,
+ sceneData: SceneData,
+ conversationHistory: ChatMessage[]
+): Promise<{ success: boolean; response?: string; error?: string }> {
+ const apiKey = getStoredApiKey();
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: 'API key not configured. Please add your Gemini API key in Settings.',
+ };
+ }
+
+ const apiUrl = `${GEMINI_API_BASE}/${GEMINI_MODEL_ID}:generateContent`;
+
+ // Build conversation history for the API
+ const systemContext = buildContextPrompt(sceneData);
+
+ // Create contents array with system context and conversation history
+ const contents = [
+ // Initial context as first message
+ {
+ role: 'user',
+ parts: [{ text: systemContext }],
+ },
+ {
+ role: 'model',
+ parts: [{ text: 'I understand the temporal context. I\'m ready to be your guide through this moment in spacetime! Feel free to ask me anything about this location, time period, or how to use the Chronoscope.' }],
+ },
+ // Add conversation history
+ ...conversationHistory.map(msg => ({
+ role: msg.role,
+ parts: [{ text: msg.content }],
+ })),
+ // Add current user message
+ {
+ role: 'user',
+ parts: [{ text: message }],
+ },
+ ];
+
+ try {
+ const response = await fetch(`${apiUrl}?key=${apiKey}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ contents,
+ generationConfig: {
+ temperature: 0.7,
+ topK: 40,
+ topP: 0.95,
+ maxOutputTokens: 1024,
+ },
+ safetySettings: [
+ {
+ category: 'HARM_CATEGORY_HARASSMENT',
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE',
+ },
+ {
+ category: 'HARM_CATEGORY_HATE_SPEECH',
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE',
+ },
+ {
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE',
+ },
+ {
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
+ threshold: 'BLOCK_MEDIUM_AND_ABOVE',
+ },
+ ],
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ return {
+ success: false,
+ error: errorData.error?.message || `API error: ${response.status}`,
+ };
+ }
+
+ const data: GeminiChatResponse = await response.json();
+
+ // Extract text response
+ const textPart = data.candidates?.[0]?.content?.parts?.find(part => part.text);
+ if (!textPart?.text) {
+ return {
+ success: false,
+ error: 'No response generated',
+ };
+ }
+
+ return {
+ success: true,
+ response: textPart.text,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
+ };
+ }
+}
+
+/**
+ * Suggested questions based on scene data
+ */
+export function getSuggestedQuestions(sceneData: SceneData): string[] {
+ const { anthropology, safety, coordinates } = sceneData;
+ const suggestions: string[] = [];
+
+ // Era-specific questions
+ suggestions.push(`What was daily life like during the ${anthropology.technologyLevel} era?`);
+
+ // Location-specific
+ if (anthropology.civilization !== 'Unknown') {
+ suggestions.push(`Tell me about ${anthropology.civilization} civilization`);
+ }
+
+ // Event-specific
+ if (anthropology.notableEvents.length > 0) {
+ suggestions.push(`What happened at ${anthropology.notableEvents[0]}?`);
+ }
+
+ // Safety-related
+ if (safety.hazardLevel === 'high' || safety.hazardLevel === 'critical') {
+ suggestions.push('Why is this moment considered dangerous?');
+ }
+
+ // Time period suggestions
+ const year = coordinates.temporal.year;
+ if (year < 0) {
+ suggestions.push('What technologies existed in this ancient period?');
+ } else if (year < 1500) {
+ suggestions.push('What were the major powers of this era?');
+ } else if (year < 1900) {
+ suggestions.push('What innovations changed life in this century?');
+ } else {
+ suggestions.push('How does this moment compare to today?');
+ }
+
+ // Navigation suggestions - encourage exploration of related historical moments
+ suggestions.push('Take me to related moments in history I can explore');
+
+ return suggestions.slice(0, 4); // Return max 4 suggestions
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 185c4d2..1800374 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -167,3 +167,16 @@ export interface GalleryImage {
description: string;
timestamp: number; // Unix timestamp when saved
}
+
+// Chat/Temporal Assistant types
+export interface ChatMessage {
+ role: 'user' | 'model';
+ content: string;
+ timestamp: number;
+}
+
+export interface ChatState {
+ messages: ChatMessage[];
+ isLoading: boolean;
+ error: string | null;
+}
diff --git a/src/utils/markdownLinks.tsx b/src/utils/markdownLinks.tsx
new file mode 100644
index 0000000..bbb7332
--- /dev/null
+++ b/src/utils/markdownLinks.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+/**
+ * Parse markdown links and render them as clickable elements
+ * Handles format: [text](url)
+ */
+export function renderMessageWithLinks(content: string): React.ReactNode {
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
+ const matches = content.matchAll(linkPattern);
+ const matchArray = Array.from(matches);
+
+ if (matchArray.length === 0) {
+ return content;
+ }
+
+ const parts: React.ReactNode[] = [];
+ let lastIndex = 0;
+
+ for (const match of matchArray) {
+ const matchIndex = match.index ?? 0;
+
+ // Add text before the link
+ if (matchIndex > lastIndex) {
+ parts.push(content.slice(lastIndex, matchIndex));
+ }
+
+ const [fullMatch, linkText, linkUrl] = match;
+
+ // Create clickable link
+ parts.push(
+ {
+ // For internal coordinate links, navigate properly
+ if (linkUrl.startsWith('/?')) {
+ e.preventDefault();
+ window.location.href = linkUrl;
+ }
+ }}
+ >
+ {linkText}
+
+ );
+
+ lastIndex = matchIndex + fullMatch.length;
+ }
+
+ // Add remaining text after the last link
+ if (lastIndex < content.length) {
+ parts.push(content.slice(lastIndex));
+ }
+
+ return parts;
+}