summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-12-23 14:43:23 +0000
committersoryu <soryu@soryu.co>2025-12-23 14:47:18 +0000
commit555061b179b8ec034cb70f9a2dd6c823ced0f637 (patch)
tree0545b4395dab6d957884d8d36bf15b8da529dc1f /makima/frontend
parenta32dc56d2e5447ef8988cb98b8686476cc94e70c (diff)
downloadsoryu-555061b179b8ec034cb70f9a2dd6c823ced0f637.tar.gz
soryu-555061b179b8ec034cb70f9a2dd6c823ced0f637.zip
Add file body and initial tool call system
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/package-lock.json366
-rw-r--r--makima/frontend/package.json3
-rw-r--r--makima/frontend/src/components/charts/ChartRenderer.tsx181
-rw-r--r--makima/frontend/src/components/files/BodyRenderer.tsx125
-rw-r--r--makima/frontend/src/components/files/CliInput.tsx168
-rw-r--r--makima/frontend/src/components/files/FileDetail.tsx91
-rw-r--r--makima/frontend/src/lib/api.ts57
-rw-r--r--makima/frontend/src/routes/files.tsx41
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
9 files changed, 999 insertions, 35 deletions
diff --git a/makima/frontend/package-lock.json b/makima/frontend/package-lock.json
index 1b793bb..e305d2a 100644
--- a/makima/frontend/package-lock.json
+++ b/makima/frontend/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-router": "^7.1.0"
+ "react-router": "^7.1.0",
+ "recharts": "^3.6.0"
},
"devDependencies": {
"@react-router/dev": "^7.1.0",
@@ -1063,6 +1064,40 @@
}
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz",
+ "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@remix-run/node-fetch-server": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.9.0.tgz",
@@ -1361,6 +1396,16 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
@@ -1659,6 +1704,60 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1669,7 +1768,7 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1683,6 +1782,11 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -1831,6 +1935,14 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@@ -1859,7 +1971,117 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true
+ "devOptional": true
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
},
"node_modules/debug": {
"version": "4.4.3",
@@ -1878,6 +2100,11 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
"node_modules/dedent": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
@@ -1926,6 +2153,11 @@
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true
},
+ "node_modules/es-toolkit": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
+ "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="
+ },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -1976,6 +2208,11 @@
"node": ">=6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+ },
"node_modules/exit-hook": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
@@ -2040,6 +2277,23 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/isbot": {
"version": "5.1.32",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.32.tgz",
@@ -2521,6 +2775,34 @@
"react": "^19.2.3"
}
},
+ "node_modules/react-is": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
+ "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
+ "peer": true
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -2564,6 +2846,50 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/recharts": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
+ "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/rollup": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
@@ -2655,6 +2981,11 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2714,6 +3045,14 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/valibot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
@@ -2728,6 +3067,27 @@
}
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
diff --git a/makima/frontend/package.json b/makima/frontend/package.json
index 22c4c54..ef99c3d 100644
--- a/makima/frontend/package.json
+++ b/makima/frontend/package.json
@@ -13,7 +13,8 @@
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-router": "^7.1.0"
+ "react-router": "^7.1.0",
+ "recharts": "^3.6.0"
},
"devDependencies": {
"@react-router/dev": "^7.1.0",
diff --git a/makima/frontend/src/components/charts/ChartRenderer.tsx b/makima/frontend/src/components/charts/ChartRenderer.tsx
new file mode 100644
index 0000000..276b170
--- /dev/null
+++ b/makima/frontend/src/components/charts/ChartRenderer.tsx
@@ -0,0 +1,181 @@
+import { useMemo } from "react";
+import {
+ LineChart,
+ Line,
+ BarChart,
+ Bar,
+ PieChart,
+ Pie,
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ Cell,
+} from "recharts";
+import type { ChartType } from "../../lib/api";
+
+interface ChartRendererProps {
+ chartType: ChartType;
+ data: Record<string, unknown>[];
+ title?: string;
+ config?: Record<string, unknown>;
+}
+
+// Default color palette
+const COLORS = [
+ "#9bc3ff",
+ "#ff9b9b",
+ "#9bffb3",
+ "#ffeb9b",
+ "#d49bff",
+ "#9bfff0",
+ "#ff9beb",
+ "#b3ff9b",
+];
+
+export function ChartRenderer({
+ chartType,
+ data,
+ title,
+ config,
+}: ChartRendererProps) {
+ // Extract data keys (excluding 'name' which is used for labels)
+ const dataKeys = useMemo(() => {
+ if (data.length === 0) return [];
+ const keys = Object.keys(data[0]).filter((key) => key !== "name");
+ return keys;
+ }, [data]);
+
+ // Get colors from config or use defaults
+ const colors = (config?.colors as string[]) || COLORS;
+
+ const renderChart = () => {
+ switch (chartType) {
+ case "line":
+ return (
+ <LineChart data={data}>
+ <CartesianGrid strokeDasharray="3 3" stroke="#333" />
+ <XAxis dataKey="name" stroke="#9bc3ff" fontSize={12} />
+ <YAxis stroke="#9bc3ff" fontSize={12} />
+ <Tooltip
+ contentStyle={{
+ backgroundColor: "#111",
+ border: "1px solid #9bc3ff",
+ borderRadius: "0",
+ }}
+ />
+ <Legend />
+ {dataKeys.map((key, i) => (
+ <Line
+ key={key}
+ type="monotone"
+ dataKey={key}
+ stroke={colors[i % colors.length]}
+ strokeWidth={2}
+ dot={{ fill: colors[i % colors.length] }}
+ />
+ ))}
+ </LineChart>
+ );
+
+ case "bar":
+ return (
+ <BarChart data={data}>
+ <CartesianGrid strokeDasharray="3 3" stroke="#333" />
+ <XAxis dataKey="name" stroke="#9bc3ff" fontSize={12} />
+ <YAxis stroke="#9bc3ff" fontSize={12} />
+ <Tooltip
+ contentStyle={{
+ backgroundColor: "#111",
+ border: "1px solid #9bc3ff",
+ borderRadius: "0",
+ }}
+ />
+ <Legend />
+ {dataKeys.map((key, i) => (
+ <Bar key={key} dataKey={key} fill={colors[i % colors.length]} />
+ ))}
+ </BarChart>
+ );
+
+ case "area":
+ return (
+ <AreaChart data={data}>
+ <CartesianGrid strokeDasharray="3 3" stroke="#333" />
+ <XAxis dataKey="name" stroke="#9bc3ff" fontSize={12} />
+ <YAxis stroke="#9bc3ff" fontSize={12} />
+ <Tooltip
+ contentStyle={{
+ backgroundColor: "#111",
+ border: "1px solid #9bc3ff",
+ borderRadius: "0",
+ }}
+ />
+ <Legend />
+ {dataKeys.map((key, i) => (
+ <Area
+ key={key}
+ type="monotone"
+ dataKey={key}
+ stroke={colors[i % colors.length]}
+ fill={colors[i % colors.length]}
+ fillOpacity={0.3}
+ />
+ ))}
+ </AreaChart>
+ );
+
+ case "pie":
+ // For pie charts, use the first data key as value
+ const valueKey = dataKeys[0] || "value";
+ return (
+ <PieChart>
+ <Pie
+ data={data}
+ dataKey={valueKey}
+ nameKey="name"
+ cx="50%"
+ cy="50%"
+ outerRadius={80}
+ label={({ name, percent }) =>
+ `${name}: ${((percent ?? 0) * 100).toFixed(0)}%`
+ }
+ labelLine={{ stroke: "#9bc3ff" }}
+ >
+ {data.map((_, i) => (
+ <Cell key={i} fill={colors[i % colors.length]} />
+ ))}
+ </Pie>
+ <Tooltip
+ contentStyle={{
+ backgroundColor: "#111",
+ border: "1px solid #9bc3ff",
+ borderRadius: "0",
+ }}
+ />
+ <Legend />
+ </PieChart>
+ );
+
+ default:
+ return <div className="text-red-400">Unknown chart type: {chartType}</div>;
+ }
+ };
+
+ return (
+ <div className="w-full">
+ {title && (
+ <h4 className="text-[#9bc3ff] font-mono text-sm mb-2">{title}</h4>
+ )}
+ <div className="h-64 w-full">
+ <ResponsiveContainer width="100%" height="100%">
+ {renderChart()}
+ </ResponsiveContainer>
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/files/BodyRenderer.tsx b/makima/frontend/src/components/files/BodyRenderer.tsx
new file mode 100644
index 0000000..9d008e2
--- /dev/null
+++ b/makima/frontend/src/components/files/BodyRenderer.tsx
@@ -0,0 +1,125 @@
+import type { BodyElement } from "../../lib/api";
+import { ChartRenderer } from "../charts/ChartRenderer";
+
+interface BodyRendererProps {
+ elements: BodyElement[];
+}
+
+export function BodyRenderer({ elements }: BodyRendererProps) {
+ if (elements.length === 0) {
+ return (
+ <div className="text-[#555] font-mono text-sm italic">
+ No content yet. Use the CLI below to add content.
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-4">
+ {elements.map((element, index) => (
+ <BodyElementRenderer key={index} element={element} />
+ ))}
+ </div>
+ );
+}
+
+function BodyElementRenderer({ element }: { element: BodyElement }) {
+ switch (element.type) {
+ case "heading":
+ return <HeadingElement level={element.level} text={element.text} />;
+ case "paragraph":
+ return <ParagraphElement text={element.text} />;
+ case "chart":
+ return (
+ <ChartElement
+ chartType={element.chartType}
+ data={element.data}
+ title={element.title}
+ config={element.config}
+ />
+ );
+ case "image":
+ return (
+ <ImageElement
+ src={element.src}
+ alt={element.alt}
+ caption={element.caption}
+ />
+ );
+ default:
+ return null;
+ }
+}
+
+function HeadingElement({ level, text }: { level: number; text: string }) {
+ const className = "font-mono text-[#9bc3ff]";
+
+ switch (level) {
+ case 1:
+ return <h1 className={`${className} text-2xl font-bold`}>{text}</h1>;
+ case 2:
+ return <h2 className={`${className} text-xl font-bold`}>{text}</h2>;
+ case 3:
+ return <h3 className={`${className} text-lg font-semibold`}>{text}</h3>;
+ case 4:
+ return <h4 className={`${className} text-base font-semibold`}>{text}</h4>;
+ case 5:
+ return <h5 className={`${className} text-sm font-semibold`}>{text}</h5>;
+ case 6:
+ return <h6 className={`${className} text-xs font-semibold`}>{text}</h6>;
+ default:
+ return <h3 className={`${className} text-lg font-semibold`}>{text}</h3>;
+ }
+}
+
+function ParagraphElement({ text }: { text: string }) {
+ return <p className="font-mono text-sm text-white/80 leading-relaxed">{text}</p>;
+}
+
+function ChartElement({
+ chartType,
+ data,
+ title,
+ config,
+}: {
+ chartType: "line" | "bar" | "pie" | "area";
+ data: Record<string, unknown>[];
+ title?: string;
+ config?: Record<string, unknown>;
+}) {
+ return (
+ <div className="border border-[#333] p-4 bg-black/30">
+ <ChartRenderer
+ chartType={chartType}
+ data={data}
+ title={title}
+ config={config}
+ />
+ </div>
+ );
+}
+
+function ImageElement({
+ src,
+ alt,
+ caption,
+}: {
+ src: string;
+ alt?: string;
+ caption?: string;
+}) {
+ return (
+ <figure className="space-y-2">
+ <img
+ src={src}
+ alt={alt || ""}
+ className="max-w-full border border-[#333]"
+ />
+ {caption && (
+ <figcaption className="text-[#555] font-mono text-xs italic">
+ {caption}
+ </figcaption>
+ )}
+ </figure>
+ );
+}
diff --git a/makima/frontend/src/components/files/CliInput.tsx b/makima/frontend/src/components/files/CliInput.tsx
new file mode 100644
index 0000000..b20eb27
--- /dev/null
+++ b/makima/frontend/src/components/files/CliInput.tsx
@@ -0,0 +1,168 @@
+import { useState, useCallback, useRef, useEffect } from "react";
+import { chatWithFile, type BodyElement } from "../../lib/api";
+
+interface CliInputProps {
+ fileId: string;
+ onUpdate: (body: BodyElement[], summary: string | null) => void;
+}
+
+interface Message {
+ id: string;
+ type: "user" | "assistant" | "error";
+ content: string;
+ toolCalls?: { name: string; success: boolean; message: string }[];
+}
+
+export function CliInput({ fileId, onUpdate }: CliInputProps) {
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [messages, setMessages] = useState<Message[]>([]);
+ const [expanded, setExpanded] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const messagesRef = useRef<HTMLDivElement>(null);
+
+ // Auto-scroll to bottom when messages change
+ useEffect(() => {
+ if (messagesRef.current) {
+ messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!input.trim() || loading) return;
+
+ const userMessage = input.trim();
+ setInput("");
+ setExpanded(true);
+
+ // Add user message
+ const userMsgId = Date.now().toString();
+ setMessages((prev) => [
+ ...prev,
+ { id: userMsgId, type: "user", content: userMessage },
+ ]);
+
+ setLoading(true);
+
+ try {
+ const response = await chatWithFile(fileId, userMessage);
+
+ // Add assistant response
+ const assistantMsgId = (Date.now() + 1).toString();
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: assistantMsgId,
+ type: "assistant",
+ content: response.response,
+ toolCalls: response.toolCalls.map((tc) => ({
+ name: tc.name,
+ success: tc.result.success,
+ message: tc.result.message,
+ })),
+ },
+ ]);
+
+ // Update parent with new body/summary
+ onUpdate(response.updatedBody, response.updatedSummary);
+ } catch (err) {
+ const errorMsgId = (Date.now() + 1).toString();
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: errorMsgId,
+ type: "error",
+ content: err instanceof Error ? err.message : "An error occurred",
+ },
+ ]);
+ } finally {
+ setLoading(false);
+ inputRef.current?.focus();
+ }
+ },
+ [input, loading, fileId, onUpdate]
+ );
+
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ }, []);
+
+ return (
+ <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]">
+ {/* Messages Panel (expandable) */}
+ {expanded && messages.length > 0 && (
+ <div
+ ref={messagesRef}
+ className="max-h-48 overflow-y-auto p-3 space-y-2 border-b border-[rgba(117,170,252,0.2)]"
+ >
+ {messages.map((msg) => (
+ <div key={msg.id} className="font-mono text-xs">
+ {msg.type === "user" && (
+ <div className="flex gap-2">
+ <span className="text-[#9bc3ff]">&gt;</span>
+ <span className="text-white/80">{msg.content}</span>
+ </div>
+ )}
+ {msg.type === "assistant" && (
+ <div className="pl-4 space-y-1">
+ <div className="text-[#75aafc]">{msg.content}</div>
+ {msg.toolCalls && msg.toolCalls.length > 0 && (
+ <div className="text-[#555] text-[10px] space-y-0.5">
+ {msg.toolCalls.map((tc, i) => (
+ <div key={i}>
+ <span
+ className={
+ tc.success ? "text-green-500" : "text-red-400"
+ }
+ >
+ {tc.success ? "+" : "x"}
+ </span>{" "}
+ {tc.name}: {tc.message}
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ {msg.type === "error" && (
+ <div className="pl-4 text-red-400">{msg.content}</div>
+ )}
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* Input Bar */}
+ <form onSubmit={handleSubmit} className="flex items-center gap-2 p-3">
+ <span className="text-[#9bc3ff] font-mono text-sm">$</span>
+ <input
+ ref={inputRef}
+ type="text"
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ placeholder={loading ? "Processing..." : "Add a heading, chart, or summary..."}
+ disabled={loading}
+ className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]"
+ />
+ {messages.length > 0 && (
+ <button
+ type="button"
+ onClick={clearMessages}
+ className="text-[#555] hover:text-[#9bc3ff] font-mono text-xs transition-colors"
+ >
+ clear
+ </button>
+ )}
+ <button
+ type="submit"
+ disabled={loading || !input.trim()}
+ className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {loading ? "..." : "Send"}
+ </button>
+ </form>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/files/FileDetail.tsx b/makima/frontend/src/components/files/FileDetail.tsx
index 643f35e..ffc67dd 100644
--- a/makima/frontend/src/components/files/FileDetail.tsx
+++ b/makima/frontend/src/components/files/FileDetail.tsx
@@ -1,5 +1,6 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import type { FileDetail as FileDetailType } from "../../lib/api";
+import { BodyRenderer } from "./BodyRenderer";
interface FileDetailProps {
file: FileDetailType;
@@ -19,6 +20,13 @@ export function FileDetail({
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(file.name);
const [description, setDescription] = useState(file.description || "");
+ const [transcriptExpanded, setTranscriptExpanded] = useState(false);
+
+ // Update local state when file changes
+ useEffect(() => {
+ setName(file.name);
+ setDescription(file.description || "");
+ }, [file.name, file.description]);
const handleSave = () => {
onSave(file.id, name, description);
@@ -116,27 +124,70 @@ export function FileDetail({
)}
</div>
- {/* Transcript */}
- <div className="flex-1 overflow-y-auto p-4 space-y-3">
- {file.transcript.length === 0 ? (
- <div className="text-center text-[#9bc3ff] text-sm font-mono opacity-60 py-8">
- No transcript entries.
+ {/* Content */}
+ <div className="flex-1 overflow-y-auto p-4 space-y-6">
+ {/* Summary Section */}
+ {file.summary && (
+ <div className="border-l-2 border-[#9bc3ff] pl-4">
+ <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">
+ Summary
+ </h3>
+ <p className="font-mono text-sm text-[#dbe7ff] leading-relaxed">
+ {file.summary}
+ </p>
</div>
- ) : (
- file.transcript.map((entry) => (
- <div key={entry.id} className="font-mono text-sm">
- <div className="flex items-baseline gap-2 mb-1">
- <span className="text-[#75aafc] text-xs">
- [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s]
- </span>
- <span className="text-[#9bc3ff] text-xs font-bold">
- {entry.speaker}
- </span>
- </div>
- <p className="m-0 text-[#dbe7ff] leading-relaxed">{entry.text}</p>
- </div>
- ))
)}
+
+ {/* Body Content */}
+ <div>
+ <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-3">
+ Content
+ </h3>
+ <BodyRenderer elements={file.body} />
+ </div>
+
+ {/* Collapsible Transcript Section */}
+ <div className="border-t border-dashed border-[rgba(117,170,252,0.35)] pt-4">
+ <button
+ onClick={() => setTranscriptExpanded(!transcriptExpanded)}
+ className="flex items-center gap-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors uppercase w-full text-left"
+ >
+ <span
+ className={`transition-transform ${
+ transcriptExpanded ? "rotate-90" : ""
+ }`}
+ >
+ &gt;
+ </span>
+ Transcript ({file.transcript.length} entries)
+ </button>
+
+ {transcriptExpanded && (
+ <div className="mt-4 space-y-3 pl-4">
+ {file.transcript.length === 0 ? (
+ <div className="text-[#9bc3ff] text-sm font-mono opacity-60">
+ No transcript entries.
+ </div>
+ ) : (
+ file.transcript.map((entry) => (
+ <div key={entry.id} className="font-mono text-sm">
+ <div className="flex items-baseline gap-2 mb-1">
+ <span className="text-[#75aafc] text-xs">
+ [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s]
+ </span>
+ <span className="text-[#9bc3ff] text-xs font-bold">
+ {entry.speaker}
+ </span>
+ </div>
+ <p className="m-0 text-[#dbe7ff] leading-relaxed">
+ {entry.text}
+ </p>
+ </div>
+ ))
+ )}
+ </div>
+ )}
+ </div>
</div>
</div>
);
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index ec596ce..5ef9c22 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -49,6 +49,22 @@ export interface TranscriptEntry {
isFinal: boolean;
}
+// Chart types for visualization
+export type ChartType = "line" | "bar" | "pie" | "area";
+
+// Body element types for structured content
+export type BodyElement =
+ | { type: "heading"; level: number; text: string }
+ | { type: "paragraph"; text: string }
+ | {
+ type: "chart";
+ chartType: ChartType;
+ title?: string;
+ data: Record<string, unknown>[];
+ config?: Record<string, unknown>;
+ }
+ | { type: "image"; src: string; alt?: string; caption?: string };
+
export interface FileSummary {
id: string;
name: string;
@@ -66,6 +82,8 @@ export interface FileDetail {
description: string | null;
transcript: TranscriptEntry[];
location: string | null;
+ summary: string | null;
+ body: BodyElement[];
createdAt: string;
updatedAt: string;
}
@@ -86,6 +104,28 @@ export interface UpdateFileRequest {
name?: string;
description?: string;
transcript?: TranscriptEntry[];
+ summary?: string;
+ body?: BodyElement[];
+}
+
+// Chat API types
+export interface ChatRequest {
+ message: string;
+}
+
+export interface ToolCallInfo {
+ name: string;
+ result: {
+ success: boolean;
+ message: string;
+ };
+}
+
+export interface ChatResponse {
+ response: string;
+ toolCalls: ToolCallInfo[];
+ updatedBody: BodyElement[];
+ updatedSummary: string | null;
}
// File API functions
@@ -140,3 +180,20 @@ export async function deleteFile(id: string): Promise<void> {
throw new Error(`Failed to delete file: ${res.statusText}`);
}
}
+
+// Chat API function
+export async function chatWithFile(
+ id: string,
+ message: string
+): Promise<ChatResponse> {
+ const res = await fetch(`${API_BASE}/api/v1/files/${id}/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Chat failed: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx
index 86a24b8..00c334d 100644
--- a/makima/frontend/src/routes/files.tsx
+++ b/makima/frontend/src/routes/files.tsx
@@ -3,8 +3,9 @@ import { useParams, useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { FileList } from "../components/files/FileList";
import { FileDetail } from "../components/files/FileDetail";
+import { CliInput } from "../components/files/CliInput";
import { useFiles } from "../hooks/useFiles";
-import type { FileDetail as FileDetailType } from "../lib/api";
+import type { FileDetail as FileDetailType, BodyElement } from "../lib/api";
export default function FilesPage() {
const { id } = useParams<{ id: string }>();
@@ -58,25 +59,45 @@ export default function FilesPage() {
[editFile, fetchFile]
);
+ const handleBodyUpdate = useCallback(
+ (body: BodyElement[], summary: string | null) => {
+ if (fileDetail) {
+ setFileDetail({
+ ...fileDetail,
+ body,
+ summary,
+ });
+ }
+ },
+ [fileDetail]
+ );
+
return (
<div className="relative z-10 h-screen flex flex-col overflow-hidden">
<Masthead showTicker={false} showNav />
- <main className="flex-1 p-4 md:p-6 min-h-0 overflow-hidden">
+ <main className="flex-1 p-4 md:p-6 min-h-0 overflow-hidden flex flex-col">
{error && (
- <div className="mb-4 p-3 border border-red-400/50 bg-red-400/10 text-red-400 font-mono text-sm">
+ <div className="mb-4 p-3 border border-red-400/50 bg-red-400/10 text-red-400 font-mono text-sm shrink-0">
{error}
</div>
)}
{id && fileDetail ? (
- <FileDetail
- file={fileDetail}
- loading={detailLoading}
- onBack={handleBack}
- onSave={handleSave}
- onDelete={handleDelete}
- />
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
+ <div className="flex-1 min-h-0 overflow-hidden">
+ <FileDetail
+ file={fileDetail}
+ loading={detailLoading}
+ onBack={handleBack}
+ onSave={handleSave}
+ onDelete={handleDelete}
+ />
+ </div>
+ <div className="shrink-0">
+ <CliInput fileId={id} onUpdate={handleBodyUpdate} />
+ </div>
+ </div>
) : id && detailLoading ? (
<div className="panel h-full flex items-center justify-center">
<div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 9bb4ba8..b2542f9 100644
--- a/makima/frontend/tsconfig.tsbuildinfo
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/rewritelink.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/hooks/usefiles.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetextscramble.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/routes/_index.tsx","./src/routes/files.tsx","./src/routes/listen.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/rewritelink.tsx","./src/components/charts/chartrenderer.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/hooks/usefiles.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetextscramble.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/routes/_index.tsx","./src/routes/files.tsx","./src/routes/listen.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file