coaia-visualizer 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile.app +50 -0
  3. package/Dockerfile.test +24 -0
  4. package/LIVE_MODE_DESIGN.md +435 -0
  5. package/MCP_TESTING_COMPLETE.md +302 -0
  6. package/MCP_TESTING_IMPLEMENTATION_SUMMARY.md +317 -0
  7. package/MCP_TESTING_SETUP.md +268 -0
  8. package/NAMING.md +218 -0
  9. package/QUICK_START_MCP_TESTING.md +236 -0
  10. package/WS__issue_8__coaia-visualizer__260207.code-workspace +45 -0
  11. package/app/api/audio/[filename]/route.ts +37 -0
  12. package/app/api/charts/[id]/route.ts +48 -35
  13. package/app/api/watch/route.ts +42 -0
  14. package/app/page.tsx +103 -53
  15. package/cli.ts +56 -3
  16. package/components/add-master-chart.tsx +230 -0
  17. package/components/chart-detail-editable.tsx +27 -16
  18. package/components/chart-list.tsx +13 -1
  19. package/components/create-chart-form.tsx +248 -0
  20. package/components/data-stats.tsx +9 -7
  21. package/components/live-indicator.tsx +14 -0
  22. package/components/ui/dialog.tsx +143 -0
  23. package/components/ui/label.tsx +24 -0
  24. package/direct-test.sh +180 -0
  25. package/dist/cli.js +52 -3
  26. package/docker-compose.test.yml +69 -0
  27. package/hooks/use-live-polling.ts +45 -0
  28. package/jgwill.coaia-visualizer-8--496dca71-d476-4ac9-ba9f-376add118dd8--260208.txt +2612 -0
  29. package/lib/chart-editor.ts +281 -68
  30. package/mcp/Dockerfile +21 -0
  31. package/mcp/README.md +25 -6
  32. package/mcp/src/api-client.ts +15 -3
  33. package/mcp/src/index.ts +17 -2
  34. package/mcp/src/tools/index.ts +21 -1
  35. package/mcp/test_mcp/.gemini/settings.json +18 -0
  36. package/mcp-config.json +14 -0
  37. package/package.json +2 -2
  38. package/run-mcp-tests.sh +99 -0
  39. package/samples/tradingchart.jsonl +31 -0
  40. package/test-data/test-master.jsonl +11 -0
  41. package/test-run.log +101 -0
  42. package/test-scripts/README.md +239 -0
  43. package/test-scripts/run-all-tests.sh +38 -0
  44. package/test-scripts/test-01-basic-operations.sh +87 -0
  45. package/test-scripts/test-02-telescope-creation.sh +91 -0
  46. package/test-scripts/test-03-navigation.sh +87 -0
  47. package/validate-mcp.sh +136 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ },
6
+ {
7
+ "path": "../../../src/_sessiondata/496dca71-d476-4ac9-ba9f-376add118dd8"
8
+ },
9
+ {
10
+ "path": "../../../src/_sessiondata/55158baa-dea7-45c8-9941-540433cfe551"
11
+ },
12
+ {
13
+ "path": "../../../cesaret/book/_/tcc/winter_solstice/drop/ceremonies/4c8623a1-c2e1-4ebb-a7f3-ab8ef8376172/plans"
14
+ },
15
+ {
16
+ "path": "../../../home/mia/workspace/stcmastery-copilot-acp-55158baa-dea7-45c8-9941-540433cfe551--2602072108"
17
+ }
18
+ ],
19
+ "settings": {
20
+ "mcp": {
21
+ "servers": {
22
+ "charts_55158baa-dea7-45c8-9941-540433cfe551__LiveNarrativeWitnessMode": {
23
+ "command": "npx",
24
+ "args": [
25
+ "-y",
26
+ "coaia-narrative@0.10.1",
27
+ "--memory-path",
28
+ "${input:memory_path}"
29
+ ],
30
+ "env": {
31
+ "COAIA_TOOLS": "STC_TOOLS,NARRATIVE_TOOLS,CORE_TOOLS"
32
+ },
33
+ "type": "stdio"
34
+ }
35
+ },
36
+ "inputs": [
37
+ {
38
+ "id": "memory_path",
39
+ "type": "promptString",
40
+ "description": "Memory File Path"
41
+ }
42
+ ]
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,37 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { promises as fs } from 'fs'
3
+ import path from 'path'
4
+
5
+ export async function GET(
6
+ request: NextRequest,
7
+ { params }: { params: { filename: string } }
8
+ ) {
9
+ const audioDir = process.env.COAIAV_AUDIO_DIR || './audio'
10
+ const filename = params.filename
11
+
12
+ // Security: only allow .mp3 files
13
+ if (!filename.endsWith('.mp3')) {
14
+ return NextResponse.json(
15
+ { error: 'Only MP3 files allowed' },
16
+ { status: 400 }
17
+ )
18
+ }
19
+
20
+ try {
21
+ const audioPath = path.join(audioDir, filename)
22
+ const file = await fs.readFile(audioPath)
23
+
24
+ return new NextResponse(file, {
25
+ headers: {
26
+ 'Content-Type': 'audio/mpeg',
27
+ 'Content-Length': file.length.toString(),
28
+ 'Cache-Control': 'public, max-age=31536000'
29
+ }
30
+ })
31
+ } catch (error: any) {
32
+ return NextResponse.json(
33
+ { error: `Audio file not found: ${filename}` },
34
+ { status: 404 }
35
+ )
36
+ }
37
+ }
@@ -24,14 +24,14 @@ export const GET = withAuth(async (
24
24
  const content = await fs.readFile(memoryPath, 'utf-8')
25
25
  const records = parseJSONL(content)
26
26
  const parsedData = organizeData(records)
27
-
27
+
28
28
  const chartId = params.id
29
29
  const chart = parsedData.charts.find(c => c.id === chartId)
30
-
30
+
31
31
  if (!chart) {
32
32
  return errorResponse(`Chart not found: ${chartId}`, 404)
33
33
  }
34
-
34
+
35
35
  return successResponse({ chart })
36
36
  } catch (error: any) {
37
37
  return errorResponse(`Failed to read chart: ${error.message}`, 500)
@@ -48,11 +48,14 @@ export const GET = withAuth(async (
48
48
  * addCurrentRealityObservation?: string
49
49
  * updateCurrentRealityObservation?: { index: number, text: string }
50
50
  * deleteCurrentRealityObservation?: number
51
- * addActionStep?: { description: string, dueDate?: string }
51
+ * addActionStep?: { description: string, currentReality: string, dueDate?: string }
52
52
  * updateActionStep?: { actionName: string, description: string }
53
53
  * toggleActionCompletion?: string (action name)
54
+ * markActionComplete?: string (action name)
55
+ * updateActionProgress?: { actionStepName: string, progressObservation: string, updateCurrentReality?: boolean }
54
56
  * updateActionDueDate?: { actionName: string, dueDate: string | null }
55
57
  * deleteActionStep?: string (action name)
58
+ * createTelescopedChart?: { actionName: string }
56
59
  * updateDueDate?: string | null
57
60
  * }
58
61
  */
@@ -69,53 +72,53 @@ export const POST = withAuth(async (
69
72
  try {
70
73
  const chartId = params.id
71
74
  const body = await request.json()
72
-
75
+
73
76
  // Read current data
74
77
  const content = await fs.readFile(memoryPath, 'utf-8')
75
78
  const records = parseJSONL(content)
76
79
  const parsedData = organizeData(records)
77
-
80
+
78
81
  // Verify chart exists
79
82
  const chart = parsedData.charts.find(c => c.id === chartId)
80
83
  if (!chart) {
81
84
  return errorResponse(`Chart not found: ${chartId}`, 404)
82
85
  }
83
-
86
+
84
87
  // Create editor
85
88
  const editor = new ChartEditor(parsedData)
86
-
89
+
87
90
  // Apply updates based on body
88
91
  const updates: string[] = []
89
-
92
+
90
93
  if (body.updateDesiredOutcome) {
91
94
  editor.updateDesiredOutcome(chartId, body.updateDesiredOutcome)
92
95
  updates.push('Updated desired outcome')
93
96
  }
94
-
97
+
95
98
  if (body.addCurrentRealityObservation) {
96
99
  editor.addCurrentRealityObservation(chartId, body.addCurrentRealityObservation)
97
100
  updates.push('Added current reality observation')
98
101
  }
99
102
 
100
103
  if (body.updateCurrentReality) {
101
- const observations = Array.isArray(body.updateCurrentReality)
102
- ? body.updateCurrentReality
104
+ const observations = Array.isArray(body.updateCurrentReality)
105
+ ? body.updateCurrentReality
103
106
  : [body.updateCurrentReality]
104
107
  editor.updateCurrentReality(chartId, observations)
105
108
  updates.push(`Updated current reality with ${observations.length} observation(s)`)
106
109
  }
107
-
110
+
108
111
  if (body.updateCurrentRealityObservation) {
109
112
  const { index, text } = body.updateCurrentRealityObservation
110
113
  editor.updateCurrentRealityObservation(chartId, index, text)
111
114
  updates.push(`Updated current reality observation ${index}`)
112
115
  }
113
-
116
+
114
117
  if (typeof body.deleteCurrentRealityObservation === 'number') {
115
118
  editor.deleteCurrentRealityObservation(chartId, body.deleteCurrentRealityObservation)
116
119
  updates.push(`Deleted current reality observation ${body.deleteCurrentRealityObservation}`)
117
120
  }
118
-
121
+
119
122
  if (body.addActionStep) {
120
123
  const { description, currentReality, dueDate } = body.addActionStep
121
124
  if (!currentReality) {
@@ -124,13 +127,13 @@ export const POST = withAuth(async (
124
127
  const result = editor.addActionStep(chartId, description, currentReality, dueDate)
125
128
  updates.push(`Added action step as telescoped chart ${result.chartId}`)
126
129
  }
127
-
130
+
128
131
  if (body.updateActionStep) {
129
132
  const { actionName, description } = body.updateActionStep
130
133
  editor.updateActionStep(chartId, actionName, description)
131
134
  updates.push(`Updated action step ${actionName}`)
132
135
  }
133
-
136
+
134
137
  if (body.toggleActionCompletion) {
135
138
  editor.toggleActionCompletion(body.toggleActionCompletion)
136
139
  updates.push(`Toggled completion for ${body.toggleActionCompletion}`)
@@ -146,44 +149,54 @@ export const POST = withAuth(async (
146
149
  editor.updateActionProgress(actionStepName, progressObservation, updateCurrentReality)
147
150
  updates.push(`Updated progress for ${actionStepName}`)
148
151
  }
149
-
152
+
150
153
  if (body.updateActionDueDate) {
151
154
  const { actionName, dueDate } = body.updateActionDueDate
152
155
  editor.updateActionDueDate(actionName, dueDate)
153
156
  updates.push(`Updated due date for ${actionName}`)
154
157
  }
155
-
158
+
156
159
  if (body.deleteActionStep) {
157
160
  editor.deleteActionStep(body.deleteActionStep)
158
161
  updates.push(`Deleted action step ${body.deleteActionStep}`)
159
162
  }
160
-
163
+
164
+ if (body.createTelescopedChart) {
165
+ const { actionName } = body.createTelescopedChart
166
+ const chart = parsedData.charts.find(c => c.id === chartId)
167
+ if (!chart) {
168
+ return errorResponse(`Chart not found: ${chartId}`, 404)
169
+ }
170
+ const newChartId = editor.createTelescopedChartFromAction(chart.chartEntity.name, actionName)
171
+ updates.push(`Created telescoped chart ${newChartId} from action ${actionName}`)
172
+ }
173
+
161
174
  if ('updateDueDate' in body) {
162
175
  editor.updateChartDueDate(chartId, body.updateDueDate)
163
176
  updates.push('Updated chart due date')
164
177
  }
165
-
178
+
166
179
  if (updates.length === 0) {
167
180
  return errorResponse('No valid updates provided', 400)
168
181
  }
169
-
182
+
170
183
  // Export and save
171
184
  const newContent = editor.exportToJSONL()
172
185
  const backupPath = `${memoryPath}.backup-${Date.now()}`
173
186
  await fs.copyFile(memoryPath, backupPath)
174
187
  await fs.writeFile(memoryPath, newContent, 'utf-8')
175
-
188
+
176
189
  // Get updated chart
177
190
  const updatedData = editor.getUpdatedData()
178
191
  const updatedChart = updatedData.charts.find(c => c.id === chartId)
179
-
192
+
180
193
  return successResponse({
181
194
  chart: updatedChart,
182
195
  updates,
183
196
  backup: backupPath,
184
197
  message: `Chart updated: ${updates.join(', ')}`
185
198
  })
186
-
199
+
187
200
  } catch (error: any) {
188
201
  return errorResponse(`Failed to update chart: ${error.message}`, 500)
189
202
  }
@@ -205,34 +218,34 @@ export const DELETE = withAuth(async (
205
218
 
206
219
  try {
207
220
  const chartId = params.id
208
-
221
+
209
222
  // Read current data
210
223
  const content = await fs.readFile(memoryPath, 'utf-8')
211
224
  const records = parseJSONL(content)
212
225
  const parsedData = organizeData(records)
213
-
226
+
214
227
  // Verify chart exists
215
228
  const chart = parsedData.charts.find(c => c.id === chartId)
216
229
  if (!chart) {
217
230
  return errorResponse(`Chart not found: ${chartId}`, 404)
218
231
  }
219
-
232
+
220
233
  // Collect all entities to delete
221
234
  const entitiesToDelete = new Set<string>()
222
235
  entitiesToDelete.add(chartId)
223
236
  entitiesToDelete.add(`${chartId}_desired_outcome`)
224
237
  entitiesToDelete.add(`${chartId}_current_reality`)
225
-
238
+
226
239
  // Add action steps
227
240
  chart.actions.forEach(action => {
228
241
  entitiesToDelete.add(action.name)
229
242
  })
230
-
243
+
231
244
  // Add narrative beats
232
245
  chart.narrativeBeats.forEach(beat => {
233
246
  entitiesToDelete.add(beat.name)
234
247
  })
235
-
248
+
236
249
  // Recursively delete sub-charts
237
250
  const deleteSubCharts = (subChart: typeof chart) => {
238
251
  entitiesToDelete.add(subChart.id)
@@ -243,7 +256,7 @@ export const DELETE = withAuth(async (
243
256
  subChart.subCharts.forEach(deleteSubCharts)
244
257
  }
245
258
  chart.subCharts.forEach(deleteSubCharts)
246
-
259
+
247
260
  // Filter out deleted entities and their relations
248
261
  const filteredRecords = records.filter(record => {
249
262
  if (record.type === 'entity') {
@@ -253,19 +266,19 @@ export const DELETE = withAuth(async (
253
266
  return !entitiesToDelete.has(record.from) && !entitiesToDelete.has(record.to)
254
267
  }
255
268
  })
256
-
269
+
257
270
  // Write back
258
271
  const newContent = filteredRecords.map(r => JSON.stringify(r)).join('\n') + '\n'
259
272
  const backupPath = `${memoryPath}.backup-${Date.now()}`
260
273
  await fs.copyFile(memoryPath, backupPath)
261
274
  await fs.writeFile(memoryPath, newContent, 'utf-8')
262
-
275
+
263
276
  return successResponse({
264
277
  message: `Chart ${chartId} deleted successfully`,
265
278
  deletedEntities: Array.from(entitiesToDelete),
266
279
  backup: backupPath
267
280
  })
268
-
281
+
269
282
  } catch (error: any) {
270
283
  return errorResponse(`Failed to delete chart: ${error.message}`, 500)
271
284
  }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { promises as fs } from 'fs'
3
+
4
+ export async function GET() {
5
+ const memoryPath = process.env.COAIAV_MEMORY_PATH
6
+
7
+ if (!memoryPath) {
8
+ return NextResponse.json(
9
+ { error: 'No memory file configured' },
10
+ { status: 400 }
11
+ )
12
+ }
13
+
14
+ try {
15
+ const stats = await fs.stat(memoryPath)
16
+ const content = await fs.readFile(memoryPath, 'utf-8')
17
+ const lines = content.trim().split('\n').filter(l => l.trim())
18
+
19
+ // Count narrative beats
20
+ const beatCount = lines.filter(line => {
21
+ try {
22
+ const record = JSON.parse(line)
23
+ return record.type === 'entity' &&
24
+ record.metadata?.type === 'narrative_beat'
25
+ } catch {
26
+ return false
27
+ }
28
+ }).length
29
+
30
+ return NextResponse.json({
31
+ lastModified: stats.mtime.getTime(),
32
+ beatCount,
33
+ fileSize: stats.size,
34
+ totalRecords: lines.length
35
+ })
36
+ } catch (error: any) {
37
+ return NextResponse.json(
38
+ { error: `Watch failed: ${error.message}` },
39
+ { status: 500 }
40
+ )
41
+ }
42
+ }
package/app/page.tsx CHANGED
@@ -1,4 +1,3 @@
1
- // app/page.tsx
2
1
  "use client"
3
2
 
4
3
  import { useState, useEffect } from "react"
@@ -6,14 +5,17 @@ import { FileUpload } from "@/components/file-upload"
6
5
  import { ChartList } from "@/components/chart-list"
7
6
  import { ChartDetailEditable } from "@/components/chart-detail-editable"
8
7
  import { DataStats } from "@/components/data-stats"
8
+ import { CreateChartForm } from "@/components/create-chart-form"
9
9
  import type { ParsedData, Chart } from "@/lib/types"
10
10
  import { parseJSONL, organizeData } from "@/lib/jsonl-parser"
11
11
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
12
- import { Download, Upload, RefreshCw, Save, Edit3 } from "lucide-react"
12
+ import { Download, Upload, RefreshCw, Save, Edit3, Plus } from "lucide-react"
13
13
  import { Button } from "@/components/ui/button"
14
14
  import { useToast } from "@/hooks/use-toast"
15
15
  import { Badge } from "@/components/ui/badge"
16
16
  import { ThemeToggle } from "@/components/theme-toggle"
17
+ import { LiveIndicator } from "@/components/live-indicator"
18
+ import { useLivePolling } from "@/hooks/use-live-polling"
17
19
 
18
20
  export default function Page() {
19
21
  const [data, setData] = useState<ParsedData | null>(null)
@@ -24,9 +26,19 @@ export default function Page() {
24
26
  const [isAutoLoaded, setIsAutoLoaded] = useState(false)
25
27
  const [isLoading, setIsLoading] = useState(false)
26
28
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
29
+ const [showCreateForm, setShowCreateForm] = useState(false)
27
30
  const { toast } = useToast()
28
31
 
29
- // Auto-load from API if COAIAV_MEMORY_PATH is set
32
+ // Live mode configuration
33
+ const liveMode = typeof window !== 'undefined' && process.env.NEXT_PUBLIC_LIVE_MODE === 'true'
34
+ const pollInterval = typeof window !== 'undefined' ? parseInt(process.env.NEXT_PUBLIC_POLL_INTERVAL || '2000') : 2000
35
+
36
+ const { isLive } = useLivePolling({
37
+ enabled: liveMode && isAutoLoaded,
38
+ interval: pollInterval,
39
+ onReload: handleReload
40
+ })
41
+
30
42
  useEffect(() => {
31
43
  async function autoLoad() {
32
44
  try {
@@ -55,6 +67,7 @@ export default function Page() {
55
67
  setSelectedChart(null)
56
68
  setFileName(name)
57
69
  setHasUnsavedChanges(false)
70
+ setShowCreateForm(false)
58
71
  } catch (error) {
59
72
  console.error("Failed to parse JSONL:", error)
60
73
  toast({
@@ -105,12 +118,10 @@ export default function Page() {
105
118
  try {
106
119
  const jsonlLines: string[] = []
107
120
 
108
- // Export all entities
109
121
  for (const entity of data.entities.values()) {
110
122
  jsonlLines.push(JSON.stringify(entity))
111
123
  }
112
124
 
113
- // Export all relations
114
125
  for (const relation of data.relations) {
115
126
  jsonlLines.push(JSON.stringify(relation))
116
127
  }
@@ -147,12 +158,10 @@ export default function Page() {
147
158
 
148
159
  const jsonlLines: string[] = []
149
160
 
150
- // Export all entities
151
161
  for (const entity of data.entities.values()) {
152
162
  jsonlLines.push(JSON.stringify(entity))
153
163
  }
154
164
 
155
- // Export all relations
156
165
  for (const relation of data.relations) {
157
166
  jsonlLines.push(JSON.stringify(relation))
158
167
  }
@@ -172,7 +181,6 @@ export default function Page() {
172
181
  setData(updatedData)
173
182
  setHasUnsavedChanges(true)
174
183
 
175
- // Update selected chart if it exists in new data
176
184
  if (selectedChart) {
177
185
  const updatedChart = updatedData.charts.find((c) => c.id === selectedChart.id)
178
186
  if (updatedChart) {
@@ -191,12 +199,27 @@ export default function Page() {
191
199
  const handleNavigateBack = () => {
192
200
  if (chartNavigationStack.length > 0) {
193
201
  const newStack = [...chartNavigationStack]
194
- const previousChart = newStack.pop()!
202
+ const previousChartId = newStack.pop()!.id
195
203
  setChartNavigationStack(newStack)
196
- setSelectedChart(previousChart)
204
+ // Find the refreshed parent chart from current data (with populated subCharts)
205
+ const refreshedParent = data.charts.find((c) => c.id === previousChartId)
206
+ if (refreshedParent) {
207
+ setSelectedChart(refreshedParent)
208
+ }
197
209
  }
198
210
  }
199
211
 
212
+ const handleChartCreated = (newData: ParsedData) => {
213
+ setData(newData)
214
+ setFileName("new-chart.jsonl")
215
+ setHasUnsavedChanges(true)
216
+ setShowCreateForm(false)
217
+ toast({
218
+ title: "Chart created",
219
+ description: "Your new structural tension chart has been created",
220
+ })
221
+ }
222
+
200
223
  return (
201
224
  <div className="min-h-screen bg-background">
202
225
  <header className="border-b border-border bg-card sticky top-0 z-10">
@@ -225,6 +248,7 @@ export default function Page() {
225
248
  )}
226
249
  </div>
227
250
  <div className="flex items-center gap-2 flex-wrap">
251
+ {liveMode && isAutoLoaded && <LiveIndicator isLive={isLive} />}
228
252
  <ThemeToggle />
229
253
  {data && (
230
254
  <>
@@ -274,45 +298,64 @@ export default function Page() {
274
298
 
275
299
  <main className="container mx-auto px-4 py-4 md:py-8">
276
300
  {!data ? (
277
- <div className="max-w-2xl mx-auto">
278
- <FileUpload onFileLoad={handleFileLoad} />
279
- <div className="mt-8 p-4 md:p-6 bg-muted/30 rounded-lg">
280
- <h2 className="text-lg md:text-xl font-semibold mb-3">About This Tool</h2>
281
- <p className="text-sm md:text-base text-muted-foreground leading-relaxed mb-4">
282
- This visualizer helps you explore and edit structural tension charts created by the coaia-narrative MCP
283
- server. Edit desired outcomes, add observations to current reality, manage action steps, set due dates,
284
- and save changes back to your JSONL file.
285
- </p>
286
- <div className="space-y-2">
287
- <h3 className="text-sm md:text-base font-semibold">Editing Features:</h3>
288
- <ul className="text-sm md:text-base text-muted-foreground space-y-1">
289
- <li className="flex items-start gap-2">
290
- <span className="text-chart-1 mt-1">•</span>
291
- <span>Edit desired outcomes inline</span>
292
- </li>
293
- <li className="flex items-start gap-2">
294
- <span className="text-chart-2 mt-1">•</span>
295
- <span>Add, edit, and delete current reality observations</span>
296
- </li>
297
- <li className="flex items-start gap-2">
298
- <span className="text-chart-3 mt-1">•</span>
299
- <span>Create and manage action steps</span>
300
- </li>
301
- <li className="flex items-start gap-2">
302
- <span className="text-chart-4 mt-1">•</span>
303
- <span>Set and update due dates for charts and actions</span>
304
- </li>
305
- <li className="flex items-start gap-2">
306
- <span className="text-chart-5 mt-1">•</span>
307
- <span>Toggle action completion status</span>
308
- </li>
309
- <li className="flex items-start gap-2">
310
- <span className="text-primary mt-1">•</span>
311
- <span>Auto-save to local filesystem when launched via CLI</span>
312
- </li>
313
- </ul>
314
- </div>
315
- </div>
301
+ <div className="max-w-2xl mx-auto space-y-6">
302
+ <Tabs value={showCreateForm ? "create" : "upload"} onValueChange={(v) => setShowCreateForm(v === "create")}>
303
+ <TabsList className="w-full">
304
+ <TabsTrigger value="upload" className="flex-1">
305
+ <Upload className="w-4 h-4 mr-2" />
306
+ Upload File
307
+ </TabsTrigger>
308
+ <TabsTrigger value="create" className="flex-1">
309
+ <Plus className="w-4 h-4 mr-2" />
310
+ Create New Chart
311
+ </TabsTrigger>
312
+ </TabsList>
313
+
314
+ <TabsContent value="upload" className="mt-6">
315
+ <FileUpload onFileLoad={handleFileLoad} />
316
+ <div className="mt-8 p-4 md:p-6 bg-muted/30 rounded-lg">
317
+ <h2 className="text-lg md:text-xl font-semibold mb-3">About This Tool</h2>
318
+ <p className="text-sm md:text-base text-muted-foreground leading-relaxed mb-4">
319
+ This visualizer helps you explore and edit structural tension charts created by the coaia-narrative
320
+ MCP server. Edit desired outcomes, add observations to current reality, manage action steps, set due
321
+ dates, and save changes back to your JSONL file.
322
+ </p>
323
+ <div className="space-y-2">
324
+ <h3 className="text-sm md:text-base font-semibold">Editing Features:</h3>
325
+ <ul className="text-sm md:text-base text-muted-foreground space-y-1">
326
+ <li className="flex items-start gap-2">
327
+ <span className="text-chart-1 mt-1">•</span>
328
+ <span>Edit desired outcomes inline</span>
329
+ </li>
330
+ <li className="flex items-start gap-2">
331
+ <span className="text-chart-2 mt-1">•</span>
332
+ <span>Add, edit, and delete current reality observations</span>
333
+ </li>
334
+ <li className="flex items-start gap-2">
335
+ <span className="text-chart-3 mt-1">•</span>
336
+ <span>Create and manage action steps</span>
337
+ </li>
338
+ <li className="flex items-start gap-2">
339
+ <span className="text-chart-4 mt-1">•</span>
340
+ <span>Set and update due dates for charts and actions</span>
341
+ </li>
342
+ <li className="flex items-start gap-2">
343
+ <span className="text-chart-5 mt-1">•</span>
344
+ <span>Toggle action completion status</span>
345
+ </li>
346
+ <li className="flex items-start gap-2">
347
+ <span className="text-primary mt-1">•</span>
348
+ <span>Auto-save to local filesystem when launched via CLI</span>
349
+ </li>
350
+ </ul>
351
+ </div>
352
+ </div>
353
+ </TabsContent>
354
+
355
+ <TabsContent value="create" className="mt-6">
356
+ <CreateChartForm onChartCreated={handleChartCreated} />
357
+ </TabsContent>
358
+ </Tabs>
316
359
  </div>
317
360
  ) : (
318
361
  <div className="space-y-4 md:space-y-6">
@@ -334,18 +377,25 @@ export default function Page() {
334
377
  selectedChart={selectedChart}
335
378
  onSelectChart={setSelectedChart}
336
379
  mode="hierarchy"
380
+ onDataUpdate={handleDataUpdate}
337
381
  />
338
382
  </TabsContent>
339
383
  <TabsContent value="list" className="mt-4">
340
- <ChartList data={data} selectedChart={selectedChart} onSelectChart={setSelectedChart} mode="list" />
384
+ <ChartList
385
+ data={data}
386
+ selectedChart={selectedChart}
387
+ onSelectChart={setSelectedChart}
388
+ mode="list"
389
+ onDataUpdate={handleDataUpdate}
390
+ />
341
391
  </TabsContent>
342
392
  </Tabs>
343
393
  </div>
344
394
  <div className="lg:col-span-2">
345
395
  {selectedChart ? (
346
- <ChartDetailEditable
347
- chart={selectedChart}
348
- data={data}
396
+ <ChartDetailEditable
397
+ chart={selectedChart}
398
+ data={data}
349
399
  onUpdate={handleDataUpdate}
350
400
  onNavigateToSubChart={handleNavigateToSubChart}
351
401
  onNavigateBack={chartNavigationStack.length > 0 ? handleNavigateBack : undefined}