From c3c243c952aa0a9977c42396f6f64906515bce9a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 24 May 2026 05:37:52 +0000 Subject: [PATCH] Add 5 UX innovations: AI Thought Stream, CTO Mode Switch, Founder-Aligned Reasoning, Build-From-Insight Flow, and Right Insight Rail - AI Thought Stream: real-time feed during analysis showing AI thinking steps - CTO Mode Switch: Strategic/Tactical/Operational dashboard views - Founder-Aligned Reasoning: personalized AI recommendations based on preferences - Build-From-Insight Flow: prominent 'Build This' CTAs on every blueprint - Right Insight Rail: CTO Whisper sidebar with contextual AI commentary Co-authored-by: Cole Collins --- app/api/analyses/[id]/run/route.ts | 10 +- app/dashboard/page.tsx | 201 +-------------- components/analysis-detail.tsx | 55 ++-- components/cto-mode-switch.tsx | 83 +++++++ components/dashboard-client.tsx | 387 +++++++++++++++++++++++++++++ components/founder-preferences.tsx | 232 +++++++++++++++++ components/insight-rail.tsx | 160 ++++++++++++ components/thought-stream.tsx | 68 +++++ 8 files changed, 983 insertions(+), 213 deletions(-) create mode 100644 components/cto-mode-switch.tsx create mode 100644 components/dashboard-client.tsx create mode 100644 components/founder-preferences.tsx create mode 100644 components/insight-rail.tsx create mode 100644 components/thought-stream.tsx diff --git a/app/api/analyses/[id]/run/route.ts b/app/api/analyses/[id]/run/route.ts index 502feb4..433b98f 100644 --- a/app/api/analyses/[id]/run/route.ts +++ b/app/api/analyses/[id]/run/route.ts @@ -196,6 +196,7 @@ export async function POST( await updateAnalysisStatus(id, 'scanning') await deleteBlueprintsByAnalysis(id) send({ status: 'scanning', progress: 10 }) + send({ thought: 'Connecting to GitHub repositories...' }) // Fetch file trees from GitHub for each repository const allFiles: { repo: string; path: string; type: string }[] = [] @@ -203,6 +204,7 @@ export async function POST( for (const repo of repositories) { try { + send({ thought: `Scanning file tree for ${repo.full_name}...` }) let treeData: Awaited> try { treeData = await getGitHubRepositoryTreeFromBranch( @@ -261,16 +263,18 @@ export async function POST( return } - send({ status: 'scanning', progress: 40 }) + send({ status: 'scanning', progress: 40, thought: `Found ${allFiles.length} source files across ${repositories.length} repositories` }) // Update to analyzing await updateAnalysisStatus(id, 'analyzing', { total_files: allFiles.length }) - send({ status: 'analyzing', progress: 50 }) + send({ status: 'analyzing', progress: 50, thought: 'Evaluating architecture patterns and reusable modules...' }) // Build file summary for AI (cap at 400 files to keep prompt reasonable) const filesToSend = allFiles.slice(0, 400) const fileSummary = filesToSend.map(f => `- ${f.repo}: ${f.path}`).join('\n') + send({ thought: 'Identifying component boundaries and shared dependencies...' }) + // Use Claude to analyze and discover app blueprints (structured tool output) const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) @@ -374,7 +378,7 @@ Constraints: ], }) - send({ status: 'analyzing', progress: 80 }) + send({ status: 'analyzing', progress: 80, thought: 'Ranking blueprints by opportunity score and feasibility...' }) // Check if response was truncated (hit max_tokens) if (aiResponse.stop_reason === 'max_tokens') { diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index b6735d2..87731b4 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,8 +1,5 @@ import { getAllRepositories, getAllAnalyses, getGapSummary, type Analysis, type Repository } from '@/lib/queries' -import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' -import { FolderGit2, Sparkles, Code2, Plus, ArrowRight, Zap, AlertCircle, Lightbulb } from 'lucide-react' -import Link from 'next/link' +import { DashboardClient } from '@/components/dashboard-client' export const dynamic = 'force-dynamic' @@ -22,195 +19,11 @@ export default async function DashboardPage() { const completedAnalyses = analyses.filter((analysis) => analysis.status === 'complete') return ( -
- {/* Header */} -
-

Dashboard

-

Your code intelligence overview

-
- - {/* Quick stats */} -
- -
-
-

Repositories

-

{repositories.length}

-
-
- -
-
-
- -
-
-

Analyses Run

-

{analyses.length}

-
-
- -
-
-
- -
-
-

Completed

-

{completedAnalyses.length}

-
-
- -
-
-
-
- - {/* Quick Actions */} -
-

Quick Actions

- - {repositories.length === 0 ? ( - -
- -
-

Connect your first repository

-

- Start by adding your GitHub repositories. We'll scan all files and prepare them for AI analysis. -

- -
- ) : ( -
- -
-
- -
- - {repositories.length} connected - -
-

Repositories

-

- Manage your connected GitHub repositories and add new ones. -

- -
- - -
-
- -
- - {completedAnalyses.length} complete - -
-

Run Analysis

-

- Let AI discover what apps you can build from your existing code. -

- -
- - -
-
- -
- - Quick ideas - -
-

Template Hub

-

- Pre-configured combinations you can assemble into products today. -

- -
- - {gapSummary.total_gaps > 0 && ( - -
-
- -
- - {gapSummary.total_gaps} open - -
-

Missing Code

-

- {Math.round(gapSummary.total_hours)} hours of features ready to build -

- -
- )} -
- )} -
- - {/* Recent Repositories */} - {repositories.length > 0 && ( -
-
-

Recent Repositories

- -
-
- {repositories.slice(0, 6).map((repo) => ( - -
-
- -
-
-

{repo.name}

-

{repo.full_name}

- {repo.language && ( - - {repo.language} - - )} -
-
-
- ))} -
-
- )} -
+ ) } diff --git a/components/analysis-detail.tsx b/components/analysis-detail.tsx index a7f0c08..f4e1f90 100644 --- a/components/analysis-detail.tsx +++ b/components/analysis-detail.tsx @@ -22,6 +22,7 @@ import { Lock, Crown, Hammer, + Rocket, } from 'lucide-react' import type { Analysis, Repository, AppBlueprint } from '@/lib/queries' import { BuildAppModal } from '@/components/build-app-modal' @@ -35,6 +36,9 @@ import { getOpportunityScore, getSuggestedFirstStep, } from '@/lib/opportunity-metrics' +import { ThoughtStream } from '@/components/thought-stream' +import { InsightRail } from '@/components/insight-rail' +import { useFounderPreferences, getFounderInsights, FounderReasoningBadges } from '@/components/founder-preferences' interface AnalysisDetailProps { analysis: Analysis @@ -90,6 +94,8 @@ export function AnalysisDetail({ ) const [localBlueprints, setLocalBlueprints] = useState(blueprints) const [runErrorMessage, setRunErrorMessage] = useState(analysis.error_message) + const [thoughts, setThoughts] = useState([]) + const { prefs: founderPrefs } = useFounderPreferences() useEffect(() => { setStatus(analysis.status) @@ -224,6 +230,7 @@ export function AnalysisDetail({ setProgress(0) setRunErrorMessage(null) setLocalBlueprints([]) + setThoughts([]) try { const response = await fetch(`/api/analyses/${analysis.id}/run`, { @@ -263,6 +270,7 @@ export function AnalysisDetail({ progress?: number blueprints?: AppBlueprint[] error?: string + thought?: string } if (typeof data.error === 'string') { @@ -280,6 +288,7 @@ export function AnalysisDetail({ setStatus(data.status) } if (data.progress !== undefined) setProgress(data.progress) + if (data.thought) setThoughts(prev => [...prev, data.thought!]) if (data.blueprints) setLocalBlueprints(data.blueprints) } catch { /* incomplete JSON chunk — wait for next read */ @@ -346,19 +355,22 @@ export function AnalysisDetail({ )} - {/* Progress */} + {/* Progress + AI Thought Stream */} {isInProgress && ( - -
-
- - {status === 'scanning' ? 'Scanning repositories...' : 'AI analyzing files...'} - - {progress}% +
+ +
+
+ + {status === 'scanning' ? 'Scanning repositories...' : 'AI analyzing files...'} + + {progress}% +
+
- -
- + + +
)} {/* Repositories */} @@ -380,8 +392,9 @@ export function AnalysisDetail({
- {/* App Blueprints */} -
+ {/* App Blueprints + Insight Rail */} +
+

Discovered App Blueprints

@@ -589,12 +602,14 @@ export function AnalysisDetail({ )}
+ + {blueprint.missing_files.length > 0 ? ( @@ -644,6 +659,14 @@ export function AnalysisDetail({ )}
+ {/* Right Insight Rail — CTO Whisper */} + {(localBlueprints.length > 0 || isInProgress) && ( + + )} +
+ {buildModalBlueprint && ( void + defaultMode?: CTOMode +} + +const modes: { + id: CTOMode + label: string + description: string + icon: typeof Lightbulb +}[] = [ + { + id: 'strategic', + label: 'Strategic', + description: 'Ideas, opportunities, architecture', + icon: Lightbulb, + }, + { + id: 'tactical', + label: 'Tactical', + description: 'Tasks, roadmaps, PRs', + icon: ListTodo, + }, + { + id: 'operational', + label: 'Operational', + description: 'Deployments, logs, monitoring', + icon: Activity, + }, +] + +export function CTOModeSwitch({ onChange, defaultMode }: CTOModeSwitchProps) { + const [active, setActive] = useState(() => { + if (defaultMode) return defaultMode + if (typeof window !== 'undefined') { + return (localStorage.getItem('repofuse_cto_mode') as CTOMode) || 'strategic' + } + return 'strategic' + }) + + useEffect(() => { + localStorage.setItem('repofuse_cto_mode', active) + onChange?.(active) + }, [active, onChange]) + + return ( +
+ {modes.map((mode) => { + const Icon = mode.icon + const isActive = active === mode.id + return ( + + ) + })} +
+ ) +} diff --git a/components/dashboard-client.tsx b/components/dashboard-client.tsx new file mode 100644 index 0000000..9cddf6c --- /dev/null +++ b/components/dashboard-client.tsx @@ -0,0 +1,387 @@ +'use client' + +import { useState, useCallback } from 'react' +import { CTOModeSwitch, type CTOMode } from '@/components/cto-mode-switch' +import { FounderPreferencesEditor, useFounderPreferences } from '@/components/founder-preferences' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + FolderGit2, + Sparkles, + Code2, + Plus, + ArrowRight, + Zap, + AlertCircle, + Lightbulb, + UserCog, + GitBranch, + BarChart3, + Shield, +} from 'lucide-react' +import Link from 'next/link' +import type { Analysis, Repository } from '@/lib/queries' + +interface DashboardClientProps { + repositories: Repository[] + analyses: Analysis[] + completedAnalyses: Analysis[] + gapSummary: { + total_gaps: number + blocking_gaps: number + total_hours: number + completed_count: number + by_category: Record + } +} + +export function DashboardClient({ + repositories, + analyses, + completedAnalyses, + gapSummary, +}: DashboardClientProps) { + const [mode, setMode] = useState('strategic') + const [showPrefs, setShowPrefs] = useState(false) + const { prefs } = useFounderPreferences() + + const handleModeChange = useCallback((m: CTOMode) => setMode(m), []) + + const hasPrefs = prefs.architecture || prefs.priority || prefs.stack.length > 0 + + return ( +
+ {/* Header + CTO Mode Switch */} +
+
+
+

Dashboard

+

Your code intelligence overview

+
+
+ + +
+
+ + {/* Founder preferences panel */} + {showPrefs && ( +
+ setShowPrefs(false)} /> +
+ )} + + {/* Mode description */} +
+ {mode === 'strategic' && ( +

Viewing strategic layer — ideas, opportunities, architecture

+ )} + {mode === 'tactical' && ( +

Viewing tactical layer — tasks, roadmaps, PRs

+ )} + {mode === 'operational' && ( +

Viewing operational layer — deployments, logs, monitoring

+ )} +
+
+ + {/* Quick stats — always visible */} +
+ +
+
+

Repositories

+

{repositories.length}

+
+
+ +
+
+
+ +
+
+

Analyses Run

+

{analyses.length}

+
+
+ +
+
+
+ +
+
+

Completed

+

{completedAnalyses.length}

+
+
+ +
+
+
+
+ + {/* Strategic Mode Content */} + {mode === 'strategic' && ( +
+

Strategic Actions

+ {repositories.length === 0 ? ( + +
+ +
+

Connect your first repository

+

+ Start by adding your GitHub repositories. We'll scan all files and prepare them for AI analysis. +

+ +
+ ) : ( +
+ +
+
+ +
+ + {completedAnalyses.length} complete + +
+

Discover Opportunities

+

+ Let AI discover what apps you can build from your existing code. +

+ +
+ + +
+
+ +
+ + Quick ideas + +
+

App Idea Chat

+

+ Brainstorm architecture and product ideas with AI. +

+ +
+ + +
+
+ +
+ + {repositories.length} connected + +
+

Repositories

+

+ Manage your connected GitHub repositories. +

+ +
+
+ )} +
+ )} + + {/* Tactical Mode Content */} + {mode === 'tactical' && ( +
+

Tactical View

+
+ +
+
+ +
+
+

Active Analyses

+

+ {analyses.filter(a => a.status === 'scanning' || a.status === 'analyzing').length} running, {completedAnalyses.length} complete +

+ +
+ + +
+
+ +
+
+

Template Hub

+

+ Pre-configured combinations for rapid assembly. +

+ +
+ + {gapSummary.total_gaps > 0 && ( + +
+
+ +
+ + {gapSummary.total_gaps} gaps + +
+

Missing Code

+

+ {Math.round(gapSummary.total_hours)} hours of buildable features. +

+ +
+ )} +
+
+ )} + + {/* Operational Mode Content */} + {mode === 'operational' && ( +
+

Operational Overview

+
+ +
+
+ +
+
+

Repository Health

+

+ {repositories.length} repos tracked across {new Set(repositories.map(r => r.language).filter(Boolean)).size} languages +

+ +
+ + +
+
+ +
+
+

Analysis History

+

+ {analyses.filter(a => a.status === 'failed').length} failed, {completedAnalyses.length} successful +

+ +
+ + +
+
+ +
+
+

Billing & Usage

+

+ Manage your plan and monitor credit usage. +

+ +
+
+
+ )} + + {/* Recent Repositories — always visible */} + {repositories.length > 0 && ( +
+
+

Recent Repositories

+ +
+
+ {repositories.slice(0, 6).map((repo) => ( + +
+
+ +
+
+

{repo.name}

+

{repo.full_name}

+ {repo.language && ( + + {repo.language} + + )} +
+
+
+ ))} +
+
+ )} +
+ ) +} diff --git a/components/founder-preferences.tsx b/components/founder-preferences.tsx new file mode 100644 index 0000000..1bf56f7 --- /dev/null +++ b/components/founder-preferences.tsx @@ -0,0 +1,232 @@ +'use client' + +import { useState, useCallback } from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { UserCog, X, Check, Sparkles } from 'lucide-react' + +export interface FounderPrefs { + architecture: 'serverless' | 'monolith' | 'microservices' | null + priority: 'speed' | 'scalability' | 'cost' | null + stack: string[] +} + +const STORAGE_KEY = 'repofuse_founder_prefs' + +const defaultPrefs: FounderPrefs = { + architecture: null, + priority: null, + stack: [], +} + +export function useFounderPreferences() { + const [prefs, setPrefs] = useState(() => { + if (typeof window === 'undefined') return defaultPrefs + try { + const saved = localStorage.getItem(STORAGE_KEY) + return saved ? JSON.parse(saved) : defaultPrefs + } catch { + return defaultPrefs + } + }) + + const save = useCallback((next: FounderPrefs) => { + setPrefs(next) + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) + }, []) + + return { prefs, save } +} + +export function getFounderInsights(prefs: FounderPrefs, blueprint: { + technologies: string[] + complexity: string + missing_files: { name: string; purpose: string }[] +}): string[] { + const insights: string[] = [] + + if (prefs.architecture === 'serverless') { + const hasServerless = blueprint.technologies.some(t => + /vercel|lambda|edge|serverless|neon/i.test(t) + ) + if (hasServerless) { + insights.push('Aligns with your serverless preference — recommending edge-first deployment') + } else { + insights.push('Consider adapting to serverless architecture to match your preferences') + } + } else if (prefs.architecture === 'microservices') { + insights.push('Can be structured as independent microservices for your architecture style') + } + + if (prefs.priority === 'speed') { + if (blueprint.complexity === 'simple') { + insights.push('Quick win — prioritizing speed-to-ship as you prefer') + } else { + insights.push('Skipping optional refactors to match your speed priority') + } + } else if (prefs.priority === 'scalability') { + insights.push('Including scalability considerations in the build plan') + } else if (prefs.priority === 'cost') { + insights.push('Optimized for cost efficiency with maximum code reuse') + } + + if (prefs.stack.length > 0) { + const matching = blueprint.technologies.filter(t => + prefs.stack.some(s => t.toLowerCase().includes(s.toLowerCase())) + ) + if (matching.length > 0) { + insights.push(`Uses ${matching.join(', ')} from your preferred stack`) + } + } + + return insights +} + +interface FounderPreferencesEditorProps { + onClose?: () => void +} + +const archOptions = [ + { value: 'serverless' as const, label: 'Serverless', desc: 'Vercel, Lambda, Edge' }, + { value: 'monolith' as const, label: 'Monolith', desc: 'Single deployable unit' }, + { value: 'microservices' as const, label: 'Microservices', desc: 'Distributed services' }, +] + +const priorityOptions = [ + { value: 'speed' as const, label: 'Speed', desc: 'Ship fast, iterate later' }, + { value: 'scalability' as const, label: 'Scalability', desc: 'Built to grow' }, + { value: 'cost' as const, label: 'Cost', desc: 'Minimize spend' }, +] + +const stackOptions = ['React', 'Next.js', 'Node.js', 'Python', 'TypeScript', 'Tailwind', 'PostgreSQL', 'Redis', 'Docker', 'Vercel'] + +export function FounderPreferencesEditor({ onClose }: FounderPreferencesEditorProps) { + const { prefs, save } = useFounderPreferences() + const [local, setLocal] = useState(prefs) + + const toggleStack = (tech: string) => { + setLocal(prev => ({ + ...prev, + stack: prev.stack.includes(tech) + ? prev.stack.filter(t => t !== tech) + : [...prev.stack, tech], + })) + } + + const handleSave = () => { + save(local) + onClose?.() + } + + return ( + +
+
+
+ +

Your Preferences

+
+ {onClose && ( + + )} +
+ +

+ The AI tailors recommendations based on how you build. This is your moat. +

+ +
+ +
+ {archOptions.map(opt => ( + + ))} +
+
+ +
+ +
+ {priorityOptions.map(opt => ( + + ))} +
+
+ +
+ +
+ {stackOptions.map(tech => ( + toggleStack(tech)} + > + {tech} + + ))} +
+
+ + +
+
+ ) +} + +export function FounderReasoningBadges({ insights }: { insights: string[] }) { + if (insights.length === 0) return null + + return ( +
+
+ + + Founder-aligned reasoning + +
+ {insights.map((insight, i) => ( +

+ {insight} +

+ ))} +
+ ) +} diff --git a/components/insight-rail.tsx b/components/insight-rail.tsx new file mode 100644 index 0000000..07714ab --- /dev/null +++ b/components/insight-rail.tsx @@ -0,0 +1,160 @@ +'use client' + +import { useMemo } from 'react' +import { Card } from '@/components/ui/card' +import { MessageSquareQuote, TrendingUp, AlertTriangle, Repeat2 } from 'lucide-react' +import type { AppBlueprint } from '@/lib/queries' + +interface InsightRailProps { + blueprints: AppBlueprint[] + isAnalyzing?: boolean +} + +interface Insight { + icon: typeof TrendingUp + text: string + type: 'opportunity' | 'warning' | 'info' +} + +function generateInsights(blueprints: AppBlueprint[]): Insight[] { + if (blueprints.length === 0) return [] + + const insights: Insight[] = [] + + const avgReuse = Math.round( + blueprints.reduce((sum, bp) => sum + bp.reuse_percentage, 0) / blueprints.length + ) + insights.push({ + icon: Repeat2, + text: `Average ${avgReuse}% code reuse across ${blueprints.length} blueprints`, + type: 'info', + }) + + const shipReady = blueprints.filter(bp => bp.missing_files.length === 0) + if (shipReady.length > 0) { + insights.push({ + icon: TrendingUp, + text: `${shipReady.length} blueprint${shipReady.length > 1 ? 's' : ''} can ship with zero new files`, + type: 'opportunity', + }) + } + + const highReuse = blueprints.filter(bp => bp.reuse_percentage >= 75) + if (highReuse.length > 0) { + insights.push({ + icon: TrendingUp, + text: `You can build a ${highReuse[0].app_type || 'app'} using ${highReuse[0].reuse_percentage}% existing components`, + type: 'opportunity', + }) + } + + const allTechs = blueprints.flatMap(bp => bp.technologies) + const techCounts = allTechs.reduce>((acc, t) => { + acc[t] = (acc[t] || 0) + 1 + return acc + }, {}) + const topTech = Object.entries(techCounts).sort((a, b) => b[1] - a[1])[0] + if (topTech && topTech[1] > 1) { + insights.push({ + icon: Repeat2, + text: `${topTech[0]} appears in ${topTech[1]} blueprints — strong foundation`, + type: 'info', + }) + } + + const complex = blueprints.filter(bp => bp.complexity === 'complex') + if (complex.length > 0) { + insights.push({ + icon: AlertTriangle, + text: `${complex.length} complex build${complex.length > 1 ? 's' : ''} detected — consider starting with simpler wins first`, + type: 'warning', + }) + } + + const duplicated = new Map() + blueprints.forEach(bp => { + bp.existing_files.forEach(f => { + duplicated.set(f.path, (duplicated.get(f.path) || 0) + 1) + }) + }) + const sharedFiles = [...duplicated.entries()].filter(([, count]) => count > 2) + if (sharedFiles.length > 0) { + insights.push({ + icon: Repeat2, + text: `${sharedFiles.length} module${sharedFiles.length > 1 ? 's are' : ' is'} duplicated across ${sharedFiles[0][1]}+ blueprints`, + type: 'info', + }) + } + + const totalMissing = blueprints.reduce((s, bp) => s + bp.missing_files.length, 0) + const totalExisting = blueprints.reduce((s, bp) => s + bp.existing_files.length, 0) + if (totalExisting > 0) { + insights.push({ + icon: TrendingUp, + text: `${totalExisting} existing files power all blueprints, only ${totalMissing} new files needed`, + type: 'opportunity', + }) + } + + const simpleBlueprints = blueprints.filter(bp => bp.complexity === 'simple' && bp.reuse_percentage >= 70) + if (simpleBlueprints.length > 0) { + insights.push({ + icon: TrendingUp, + text: 'Refactor recommended before scaling — consolidate shared utilities first', + type: 'warning', + }) + } + + return insights +} + +const typeStyles = { + opportunity: 'border-l-emerald-400/50 text-emerald-300/80', + warning: 'border-l-amber-400/50 text-amber-300/80', + info: 'border-l-cyan-400/50 text-cyan-300/80', +} + +export function InsightRail({ blueprints, isAnalyzing }: InsightRailProps) { + const insights = useMemo(() => generateInsights(blueprints), [blueprints]) + + if (insights.length === 0 && !isAnalyzing) return null + + return ( + +
+ + + CTO Whisper + + {isAnalyzing && ( + + + ANALYZING + + )} +
+
+ {isAnalyzing && insights.length === 0 && ( +

+ Gathering intelligence... +

+ )} + {insights.map((insight, i) => { + const Icon = insight.icon + return ( +
+ + {insight.text} +
+ ) + })} +
+
+ ) +} diff --git a/components/thought-stream.tsx b/components/thought-stream.tsx new file mode 100644 index 0000000..1266d20 --- /dev/null +++ b/components/thought-stream.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { Brain } from 'lucide-react' + +interface ThoughtStreamProps { + thoughts: string[] + isActive: boolean +} + +export function ThoughtStream({ thoughts, isActive }: ThoughtStreamProps) { + const scrollRef = useRef(null) + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [thoughts]) + + if (thoughts.length === 0 && !isActive) return null + + return ( +
+
+ + + AI Thought Stream + + {isActive && ( + + + LIVE + + )} +
+
+ {thoughts.map((thought, i) => { + const isLatest = i === thoughts.length - 1 + return ( +
+ > + + {thought} + +
+ ) + })} + {isActive && ( +
+ > + + . + . + . + +
+ )} +
+
+ ) +}