12
.env.example
Executable file
@@ -0,0 +1,12 @@
|
||||
# Claude Code UI Environment Configuration
|
||||
# Only includes variables that are actually used in the code
|
||||
|
||||
# =============================================================================
|
||||
# SERVER CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Backend server port (Express API + WebSocket server)
|
||||
#API server
|
||||
PORT=3008
|
||||
#Frontend port
|
||||
VITE_PORT=3009
|
||||
81
.gitignore
vendored
Normal file → Executable file
@@ -3,11 +3,14 @@ node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
dist-ssr/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
out/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
@@ -16,17 +19,7 @@ build/
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
.nyc_output
|
||||
|
||||
# IDE/Editor files
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
@@ -43,9 +36,71 @@ ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# Claude specific
|
||||
.claude/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
0
index.html
Normal file → Executable file
426
package-lock.json
generated
Normal file → Executable file
@@ -75,9 +75,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-code": {
|
||||
"version": "1.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.24.tgz",
|
||||
"integrity": "sha512-4S6ly2297ngNlto7IFZeEicS9u0yRDhocOzndWFovGBb+iUoEPKdZa/rhVk/tcyCADL6S+mMkiGQOlqFDrN3JQ==",
|
||||
"version": "1.0.43",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.43.tgz",
|
||||
"integrity": "sha512-VnuRK4s/R9ZRTkwH4gUjsp4SiBQXq7Y0B47OtgeXIZYVQYkhTW8m+E0IisFzXXFIyTQrE0SodGCpvgLhAYzGCg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"claude": "cli.js"
|
||||
@@ -109,30 +109,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
|
||||
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.4",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/helpers": "^7.27.6",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.27.4",
|
||||
"@babel/types": "^7.27.3",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -148,15 +148,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
|
||||
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.5",
|
||||
"@babel/types": "^7.27.3",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -179,6 +179,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
@@ -259,12 +268,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.3"
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -326,27 +335,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
|
||||
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.3",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
"@babel/types": "^7.28.0",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
||||
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
|
||||
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@@ -421,9 +430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
@@ -456,9 +465,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz",
|
||||
"integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==",
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
|
||||
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
@@ -497,9 +506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
|
||||
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -508,9 +517,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.37.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
|
||||
"integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
|
||||
"version": "6.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.0.tgz",
|
||||
"integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -1298,16 +1307,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -1318,23 +1323,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
@@ -1475,15 +1472,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
|
||||
"integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
|
||||
"version": "1.0.0-beta.19",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
|
||||
"integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
|
||||
"integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
|
||||
"integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1494,9 +1491,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1507,9 +1504,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1520,9 +1517,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
|
||||
"integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz",
|
||||
"integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1533,9 +1530,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
|
||||
"integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz",
|
||||
"integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1546,9 +1543,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
|
||||
"integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz",
|
||||
"integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1559,9 +1556,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
|
||||
"integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz",
|
||||
"integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1572,9 +1569,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
|
||||
"integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz",
|
||||
"integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1585,9 +1582,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1598,9 +1595,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz",
|
||||
"integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1611,9 +1608,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1624,9 +1621,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1637,9 +1634,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1650,9 +1647,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz",
|
||||
"integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1663,9 +1660,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1676,9 +1673,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
|
||||
"integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz",
|
||||
"integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1689,9 +1686,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
|
||||
"integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz",
|
||||
"integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1702,9 +1699,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1715,9 +1712,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1728,9 +1725,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
|
||||
"integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1754,18 +1751,6 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1816,9 +1801,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
|
||||
},
|
||||
"node_modules/@types/estree-jsx": {
|
||||
"version": "1.0.5",
|
||||
@@ -1878,9 +1863,9 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.23.13",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.13.tgz",
|
||||
"integrity": "sha512-U1CnDFpq6ydNqrRDS5Bdnvgso8ezwwbrmKvmAD3hmoVyRDsDU6HTtmcV+w0rZ3kElUCkKI5lY0DMvTTQ4+L3RQ==",
|
||||
"version": "4.23.14",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.14.tgz",
|
||||
"integrity": "sha512-lCseubZqjN9bFwHJdQlZEKEo2yO1tCiMMVL0gu3ZXwhqMdfnd6ky/fUCYbn8aJkW+cXKVwjEVhpKjOphNiHoNw==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
@@ -1904,15 +1889,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.23.13",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.13.tgz",
|
||||
"integrity": "sha512-y65ULzxOAfpxrA/8epoAOeCfmJXu9z0P62BbGOkITJTtU7WI59KfPbbwj35npSsMAkAmDE841qZo2I8jst/THg==",
|
||||
"version": "4.23.14",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.14.tgz",
|
||||
"integrity": "sha512-/CmlSh8LGUEZCxg/f78MEkEMehKnVklqJvJlL10AXXrO/2xOyPqHb8SK10GhwOqd0kHhHgVYp4+6oK5S+UIEuQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.23.13",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.23.14",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1934,15 +1919,15 @@
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz",
|
||||
"integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz",
|
||||
"integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.4",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.11",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.19",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.17.0"
|
||||
},
|
||||
@@ -2179,9 +2164,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.25.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
||||
"version": "4.25.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2198,8 +2183,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -2254,9 +2239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001723",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
|
||||
"integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
|
||||
"version": "1.0.30001726",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
|
||||
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2452,9 +2437,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
@@ -2756,9 +2741,9 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.167",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
|
||||
"integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
|
||||
"version": "1.5.179",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
|
||||
"integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -3156,15 +3141,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -4442,9 +4418,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz",
|
||||
"integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4560,7 +4536,7 @@
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
@@ -4572,6 +4548,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
@@ -4830,12 +4818,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
|
||||
"integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz",
|
||||
"integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@@ -4845,26 +4833,26 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.43.0",
|
||||
"@rollup/rollup-android-arm64": "4.43.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.43.0",
|
||||
"@rollup/rollup-darwin-x64": "4.43.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.43.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.43.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.43.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.43.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.43.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.43.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.43.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.43.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.43.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.43.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.44.1",
|
||||
"@rollup/rollup-android-arm64": "4.44.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.44.1",
|
||||
"@rollup/rollup-darwin-x64": "4.44.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.44.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.44.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.44.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.44.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.44.1",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.44.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.44.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.44.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.44.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.44.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.44.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -5663,6 +5651,18 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -6107,9 +6107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
0
package.json
Normal file → Executable file
0
postcss.config.js
Normal file → Executable file
0
public/convert-icons.md
Normal file → Executable file
0
public/favicon.png
Normal file → Executable file
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
0
public/favicon.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
0
public/generate-icons.js
Normal file → Executable file
1
public/icons/claude-ai-icon.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 509.64"><path fill="#D77655" d="M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.612-115.613 115.612H115.612C52.026 509.639 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#FCF2EE" fill-rule="nonzero" d="M142.27 316.619l73.655-41.326 1.238-3.589-1.238-1.996-3.589-.001-12.31-.759-42.084-1.138-36.498-1.516-35.361-1.896-8.897-1.895-8.34-10.995.859-5.484 7.482-5.03 10.717.935 23.683 1.617 35.537 2.452 25.782 1.517 38.193 3.968h6.064l.86-2.451-2.073-1.517-1.618-1.517-36.776-24.922-39.81-26.338-20.852-15.166-11.273-7.683-5.687-7.204-2.451-15.721 10.237-11.273 13.75.935 3.513.936 13.928 10.716 29.749 23.027 38.848 28.612 5.687 4.727 2.275-1.617.278-1.138-2.553-4.271-21.13-38.193-22.546-38.848-10.035-16.101-2.654-9.655c-.935-3.968-1.617-7.304-1.617-11.374l11.652-15.823 6.445-2.073 15.545 2.073 6.547 5.687 9.655 22.092 15.646 34.78 24.265 47.291 7.103 14.028 3.791 12.992 1.416 3.968 2.449-.001v-2.275l1.997-26.641 3.69-32.707 3.589-42.084 1.239-11.854 5.863-14.206 11.652-7.683 9.099 4.348 7.482 10.716-1.036 6.926-4.449 28.915-8.72 45.294-5.687 30.331h3.313l3.792-3.791 15.342-20.372 25.782-32.227 11.374-12.789 13.27-14.129 8.517-6.724 16.1-.001 11.854 17.617-5.307 18.199-16.581 21.029-13.75 17.819-19.716 26.54-12.309 21.231 1.138 1.694 2.932-.278 44.536-9.479 24.062-4.347 28.714-4.928 12.992 6.066 1.416 6.167-5.106 12.613-30.71 7.583-36.018 7.204-53.636 12.689-.657.48.758.935 24.164 2.275 10.337.556h25.301l47.114 3.514 12.309 8.139 7.381 9.959-1.238 7.583-18.957 9.655-25.579-6.066-59.702-14.205-20.474-5.106-2.83-.001v1.694l17.061 16.682 31.266 28.233 39.152 36.397 1.997 8.999-5.03 7.102-5.307-.758-34.401-25.883-13.27-11.651-30.053-25.302-1.996-.001v2.654l6.926 10.136 36.574 54.975 1.895 16.859-2.653 5.485-9.479 3.311-10.414-1.895-21.408-30.054-22.092-33.844-17.819-30.331-2.173 1.238-10.515 113.261-4.929 5.788-11.374 4.348-9.478-7.204-5.03-11.652 5.03-23.027 6.066-30.052 4.928-23.886 4.449-29.674 2.654-9.858-.177-.657-2.173.278-22.37 30.71-34.021 45.977-26.919 28.815-6.445 2.553-11.173-5.789 1.037-10.337 6.243-9.2 37.257-47.392 22.47-29.371 14.508-16.961-.101-2.451h-.859l-98.954 64.251-17.618 2.275-7.583-7.103.936-11.652 3.589-3.791 29.749-20.474-.101.102.024.101z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
0
public/icons/generate-icons.md
Normal file → Executable file
0
public/icons/icon-128x128.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-128x128.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-144x144.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-144x144.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-152x152.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-152x152.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-192x192.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-192x192.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-384x384.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-384x384.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-512x512.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-512x512.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-72x72.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-72x72.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-96x96.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
0
public/icons/icon-96x96.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/icons/icon-template.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
0
public/logo.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
0
public/manifest.json
Normal file → Executable file
0
public/screenshots/desktop-main.png
Normal file → Executable file
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
0
public/screenshots/mobile-chat.png
Normal file → Executable file
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
0
public/screenshots/tools-modal.png
Normal file → Executable file
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 295 KiB |
0
public/sw.js
Normal file → Executable file
0
server/claude-cli.js
Normal file → Executable file
@@ -1,162 +0,0 @@
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
async function spawnClaude(command, options = {}, ws) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { sessionId, projectPath, cwd, resume, toolsSettings } = options;
|
||||
let capturedSessionId = sessionId; // Track session ID throughout the process
|
||||
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
|
||||
// Use tools settings passed from frontend, or defaults
|
||||
const settings = toolsSettings || {
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
skipPermissions: false
|
||||
};
|
||||
|
||||
console.log('🔧 Using tools settings:', settings);
|
||||
|
||||
// Build Claude CLI command - start with basic flags
|
||||
const args = ['--output-format', 'stream-json', '--verbose'];
|
||||
|
||||
// Add tools settings flags
|
||||
if (settings.skipPermissions) {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
console.log('⚠️ Using --dangerously-skip-permissions');
|
||||
}
|
||||
|
||||
// Add print flag if we have a command
|
||||
if (command && command.trim()) {
|
||||
args.push('--print');
|
||||
}
|
||||
|
||||
// Add resume flag if resuming (after --print)
|
||||
if (resume && sessionId) {
|
||||
args.push('--resume', sessionId);
|
||||
}
|
||||
|
||||
// Add allowed tools
|
||||
if (settings.allowedTools && settings.allowedTools.length > 0) {
|
||||
for (const tool of settings.allowedTools) {
|
||||
args.push('--allowedTools', tool);
|
||||
console.log('✅ Allowing tool:', tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Add disallowed tools
|
||||
if (settings.disallowedTools && settings.disallowedTools.length > 0) {
|
||||
for (const tool of settings.disallowedTools) {
|
||||
args.push('--disallowedTools', tool);
|
||||
console.log('❌ Disallowing tool:', tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the command as the final argument
|
||||
if (command && command.trim()) {
|
||||
args.push(command);
|
||||
}
|
||||
|
||||
const workingDir = projectPath || cwd || process.cwd();
|
||||
console.log('Spawning Claude CLI:', 'claude', args.join(' '));
|
||||
console.log('Working directory:', workingDir);
|
||||
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
||||
|
||||
const claudeProcess = spawn('claude', args, {
|
||||
cwd: workingDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Handle stdout (streaming JSON responses)
|
||||
claudeProcess.stdout.on('data', (data) => {
|
||||
const lines = data.toString().split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
|
||||
// Capture session ID if it's in the response
|
||||
if (response.session_id && !capturedSessionId) {
|
||||
capturedSessionId = response.session_id;
|
||||
console.log('📝 Captured session ID:', capturedSessionId);
|
||||
|
||||
// Send session-created event only once for new sessions
|
||||
if (!sessionId && !sessionCreatedSent) {
|
||||
sessionCreatedSent = true;
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-created',
|
||||
sessionId: capturedSessionId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Send parsed response to WebSocket
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-response',
|
||||
data: response
|
||||
}));
|
||||
} catch (parseError) {
|
||||
// If not JSON, send as raw text
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-output',
|
||||
data: line
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle stderr
|
||||
claudeProcess.stderr.on('data', (data) => {
|
||||
console.error('Claude CLI stderr:', data.toString());
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-error',
|
||||
error: data.toString()
|
||||
}));
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
claudeProcess.on('close', (code) => {
|
||||
console.log(`Claude CLI process exited with code ${code}`);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-complete',
|
||||
exitCode: code,
|
||||
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
|
||||
}));
|
||||
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Claude CLI exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process errors
|
||||
claudeProcess.on('error', (error) => {
|
||||
console.error('Claude CLI process error:', error);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-error',
|
||||
error: error.message
|
||||
}));
|
||||
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Handle stdin for interactive mode
|
||||
if (command) {
|
||||
// For --print mode with arguments, we don't need to write to stdin
|
||||
claudeProcess.stdin.end();
|
||||
} else {
|
||||
// For interactive mode, we need to write the command to stdin if provided later
|
||||
// Keep stdin open for interactive session
|
||||
if (command !== undefined) {
|
||||
claudeProcess.stdin.write(command + '\n');
|
||||
claudeProcess.stdin.end();
|
||||
}
|
||||
// If no command provided, stdin stays open for interactive use
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
spawnClaude
|
||||
};
|
||||
160
server/index.js
Normal file → Executable file
@@ -28,8 +28,11 @@ const fs = require('fs').promises;
|
||||
const { spawn } = require('child_process');
|
||||
const os = require('os');
|
||||
const pty = require('node-pty');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually } = require('./projects');
|
||||
const { spawnClaude, abortClaudeSession } = require('./claude-cli');
|
||||
const gitRoutes = require('./routes/git');
|
||||
|
||||
// File system watcher for projects folder
|
||||
let projectsWatcher = null;
|
||||
@@ -144,6 +147,9 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '../dist')));
|
||||
|
||||
// Git API Routes
|
||||
app.use('/api/git', gitRoutes);
|
||||
|
||||
// API Routes
|
||||
app.get('/api/config', (req, res) => {
|
||||
// Always use the server's actual IP and port for WebSocket connections
|
||||
@@ -651,6 +657,156 @@ function handleShellConnection(ws) {
|
||||
console.error('❌ Shell WebSocket error:', error);
|
||||
});
|
||||
}
|
||||
// Audio transcription endpoint
|
||||
app.post('/api/transcribe', async (req, res) => {
|
||||
try {
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
// Handle multipart form data
|
||||
upload.single('audio')(req, res, async (err) => {
|
||||
if (err) {
|
||||
return res.status(400).json({ error: 'Failed to process audio file' });
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No audio file provided' });
|
||||
}
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Create form data for OpenAI
|
||||
const FormData = require('form-data');
|
||||
const formData = new FormData();
|
||||
formData.append('file', req.file.buffer, {
|
||||
filename: req.file.originalname,
|
||||
contentType: req.file.mimetype
|
||||
});
|
||||
formData.append('model', 'whisper-1');
|
||||
formData.append('response_format', 'json');
|
||||
formData.append('language', 'en');
|
||||
|
||||
// Make request to OpenAI
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
...formData.getHeaders()
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let transcribedText = data.text || '';
|
||||
|
||||
// Check if enhancement mode is enabled
|
||||
const mode = req.body.mode || 'default';
|
||||
|
||||
// If no transcribed text, return empty
|
||||
if (!transcribedText) {
|
||||
return res.json({ text: '' });
|
||||
}
|
||||
|
||||
// If default mode, return transcribed text without enhancement
|
||||
if (mode === 'default') {
|
||||
return res.json({ text: transcribedText });
|
||||
}
|
||||
|
||||
// Handle different enhancement modes
|
||||
try {
|
||||
const OpenAI = require('openai');
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
let prompt, systemMessage, temperature = 0.7, maxTokens = 800;
|
||||
|
||||
switch (mode) {
|
||||
case 'prompt':
|
||||
systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.';
|
||||
prompt = `You are an expert prompt engineer. Transform the following rough instruction into a clear, detailed, and context-aware AI prompt.
|
||||
|
||||
Your enhanced prompt should:
|
||||
1. Be specific and unambiguous
|
||||
2. Include relevant context and constraints
|
||||
3. Specify the desired output format
|
||||
4. Use clear, actionable language
|
||||
5. Include examples where helpful
|
||||
6. Consider edge cases and potential ambiguities
|
||||
|
||||
Transform this rough instruction into a well-crafted prompt:
|
||||
"${transcribedText}"
|
||||
|
||||
Enhanced prompt:`;
|
||||
break;
|
||||
|
||||
case 'vibe':
|
||||
case 'instructions':
|
||||
case 'architect':
|
||||
systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.';
|
||||
temperature = 0.5; // Lower temperature for more controlled output
|
||||
prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute.
|
||||
|
||||
IMPORTANT RULES:
|
||||
- Format as clear, step-by-step instructions
|
||||
- Add reasonable implementation details based on common patterns
|
||||
- Only include details directly related to what was asked
|
||||
- Do NOT add features or functionality not mentioned
|
||||
- Keep the original intent and scope intact
|
||||
- Use clear, actionable language an agent can follow
|
||||
|
||||
Transform this idea into agent-friendly instructions:
|
||||
"${transcribedText}"
|
||||
|
||||
Agent instructions:`;
|
||||
break;
|
||||
|
||||
default:
|
||||
// No enhancement needed
|
||||
break;
|
||||
}
|
||||
|
||||
// Only make GPT call if we have a prompt
|
||||
if (prompt) {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: systemMessage },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
temperature: temperature,
|
||||
max_tokens: maxTokens
|
||||
});
|
||||
|
||||
transcribedText = completion.choices[0].message.content || transcribedText;
|
||||
}
|
||||
|
||||
} catch (gptError) {
|
||||
console.error('GPT processing error:', gptError);
|
||||
// Fall back to original transcription if GPT fails
|
||||
}
|
||||
|
||||
res.json({ text: transcribedText });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Transcription error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Endpoint error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Serve React app for all other routes
|
||||
app.get('*', (req, res) => {
|
||||
@@ -666,9 +822,7 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
||||
|
||||
for (const entry of entries) {
|
||||
// Debug: log all entries including hidden files
|
||||
if (entry.name.startsWith('.')) {
|
||||
console.log('📁 Found hidden file/folder:', entry.name, 'at depth:', currentDepth);
|
||||
}
|
||||
|
||||
|
||||
// Skip only heavy build directories
|
||||
if (entry.name === 'node_modules' ||
|
||||
|
||||
32
server/projects.js
Normal file → Executable file
@@ -21,23 +21,37 @@ async function saveProjectConfig(config) {
|
||||
}
|
||||
|
||||
// Generate better display name from path
|
||||
function generateDisplayName(projectName) {
|
||||
async function generateDisplayName(projectName) {
|
||||
// Convert "-home-user-projects-myapp" to a readable format
|
||||
let path = projectName.replace(/-/g, '/');
|
||||
let projectPath = projectName.replace(/-/g, '/');
|
||||
|
||||
// Try to read package.json from the project path
|
||||
try {
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
const packageData = await fs.readFile(packageJsonPath, 'utf8');
|
||||
const packageJson = JSON.parse(packageData);
|
||||
|
||||
// Return the name from package.json if it exists
|
||||
if (packageJson.name) {
|
||||
return packageJson.name;
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall back to path-based naming if package.json doesn't exist or can't be read
|
||||
}
|
||||
|
||||
// If it starts with /, it's an absolute path
|
||||
if (path.startsWith('/')) {
|
||||
const parts = path.split('/').filter(Boolean);
|
||||
if (projectPath.startsWith('/')) {
|
||||
const parts = projectPath.split('/').filter(Boolean);
|
||||
if (parts.length > 3) {
|
||||
// Show last 2 folders with ellipsis: "...projects/myapp"
|
||||
return `.../${parts.slice(-2).join('/')}`;
|
||||
} else {
|
||||
// Show full path if short: "/home/user"
|
||||
return path;
|
||||
return projectPath;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
async function getProjects() {
|
||||
@@ -57,7 +71,7 @@ async function getProjects() {
|
||||
|
||||
// Get display name from config or generate one
|
||||
const customName = config[entry.name]?.displayName;
|
||||
const autoDisplayName = generateDisplayName(entry.name);
|
||||
const autoDisplayName = await generateDisplayName(entry.name);
|
||||
const fullPath = entry.name.replace(/-/g, '/');
|
||||
|
||||
const project = {
|
||||
@@ -96,7 +110,7 @@ async function getProjects() {
|
||||
const project = {
|
||||
name: projectName,
|
||||
path: null, // No physical path yet
|
||||
displayName: projectConfig.displayName || generateDisplayName(projectName),
|
||||
displayName: projectConfig.displayName || await generateDisplayName(projectName),
|
||||
fullPath: fullPath,
|
||||
isCustomName: !!projectConfig.displayName,
|
||||
isManuallyAdded: true,
|
||||
@@ -451,7 +465,7 @@ async function addProjectManually(projectPath, displayName = null) {
|
||||
name: projectName,
|
||||
path: null,
|
||||
fullPath: absolutePath,
|
||||
displayName: displayName || generateDisplayName(projectName),
|
||||
displayName: displayName || await generateDisplayName(projectName),
|
||||
isManuallyAdded: true,
|
||||
sessions: []
|
||||
};
|
||||
|
||||
388
server/routes/git.js
Executable file
@@ -0,0 +1,388 @@
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const router = express.Router();
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Helper function to get the actual project path from the encoded project name
|
||||
function getActualProjectPath(projectName) {
|
||||
// Claude stores projects with dashes instead of slashes
|
||||
// Convert "-Users-dmieloch-Dev-experiments-claudecodeui" to "/Users/dmieloch/Dev/experiments/claudecodeui"
|
||||
return projectName.replace(/-/g, '/');
|
||||
}
|
||||
|
||||
// Get git status for a project
|
||||
router.get('/status', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
console.log('Git status for project:', project, '-> path:', projectPath);
|
||||
|
||||
// Check if directory exists
|
||||
try {
|
||||
await fs.access(projectPath);
|
||||
} catch {
|
||||
console.error('Project path not found:', projectPath);
|
||||
return res.json({ error: 'Project not found' });
|
||||
}
|
||||
|
||||
// Check if it's a git repository
|
||||
try {
|
||||
await execAsync('git rev-parse --git-dir', { cwd: projectPath });
|
||||
} catch {
|
||||
console.error('Not a git repository:', projectPath);
|
||||
return res.json({ error: 'Not a git repository' });
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
|
||||
|
||||
// Get git status
|
||||
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath });
|
||||
|
||||
const modified = [];
|
||||
const added = [];
|
||||
const deleted = [];
|
||||
const untracked = [];
|
||||
|
||||
statusOutput.split('\n').forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
const status = line.substring(0, 2);
|
||||
const file = line.substring(3);
|
||||
|
||||
if (status === 'M ' || status === ' M' || status === 'MM') {
|
||||
modified.push(file);
|
||||
} else if (status === 'A ' || status === 'AM') {
|
||||
added.push(file);
|
||||
} else if (status === 'D ' || status === ' D') {
|
||||
deleted.push(file);
|
||||
} else if (status === '??') {
|
||||
untracked.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
branch: branch.trim(),
|
||||
modified,
|
||||
added,
|
||||
deleted,
|
||||
untracked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Git status error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific file
|
||||
router.get('/diff', async (req, res) => {
|
||||
const { project, file } = req.query;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Check if file is untracked
|
||||
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
||||
const isUntracked = statusOutput.startsWith('??');
|
||||
|
||||
let diff;
|
||||
if (isUntracked) {
|
||||
// For untracked files, show the entire file content as additions
|
||||
const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
|
||||
const lines = fileContent.split('\n');
|
||||
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
|
||||
lines.map(line => `+${line}`).join('\n');
|
||||
} else {
|
||||
// Get diff for tracked files
|
||||
const { stdout } = await execAsync(`git diff HEAD -- "${file}"`, { cwd: projectPath });
|
||||
diff = stdout || '';
|
||||
|
||||
// If no unstaged changes, check for staged changes
|
||||
if (!diff) {
|
||||
const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
|
||||
diff = stagedDiff;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ diff });
|
||||
} catch (error) {
|
||||
console.error('Git diff error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Commit changes
|
||||
router.post('/commit', async (req, res) => {
|
||||
const { project, message, files } = req.body;
|
||||
|
||||
if (!project || !message || !files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'Project name, commit message, and files are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Stage selected files
|
||||
for (const file of files) {
|
||||
await execAsync(`git add "${file}"`, { cwd: projectPath });
|
||||
}
|
||||
|
||||
// Commit with message
|
||||
const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git commit error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get list of branches
|
||||
router.get('/branches', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
console.log('Git branches for project:', project, '-> path:', projectPath);
|
||||
|
||||
// Get all branches
|
||||
const { stdout } = await execAsync('git branch -a', { cwd: projectPath });
|
||||
|
||||
// Parse branches
|
||||
const branches = stdout
|
||||
.split('\n')
|
||||
.map(branch => branch.trim())
|
||||
.filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
|
||||
.map(branch => {
|
||||
// Remove asterisk from current branch
|
||||
if (branch.startsWith('* ')) {
|
||||
return branch.substring(2);
|
||||
}
|
||||
// Remove remotes/ prefix
|
||||
if (branch.startsWith('remotes/origin/')) {
|
||||
return branch.substring(15);
|
||||
}
|
||||
return branch;
|
||||
})
|
||||
.filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
|
||||
|
||||
res.json({ branches });
|
||||
} catch (error) {
|
||||
console.error('Git branches error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Checkout branch
|
||||
router.post('/checkout', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Checkout the branch
|
||||
const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git checkout error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new branch
|
||||
router.post('/create-branch', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch name are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Create and checkout new branch
|
||||
const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git create branch error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent commits
|
||||
router.get('/commits', async (req, res) => {
|
||||
const { project, limit = 10 } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get commit log with stats
|
||||
const { stdout } = await execAsync(
|
||||
`git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
const commits = stdout
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => {
|
||||
const [hash, author, email, date, ...messageParts] = line.split('|');
|
||||
return {
|
||||
hash,
|
||||
author,
|
||||
email,
|
||||
date,
|
||||
message: messageParts.join('|')
|
||||
};
|
||||
});
|
||||
|
||||
// Get stats for each commit
|
||||
for (const commit of commits) {
|
||||
try {
|
||||
const { stdout: stats } = await execAsync(
|
||||
`git show --stat --format='' ${commit.hash}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
commit.stats = stats.trim().split('\n').pop(); // Get the summary line
|
||||
} catch (error) {
|
||||
commit.stats = '';
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ commits });
|
||||
} catch (error) {
|
||||
console.error('Git commits error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific commit
|
||||
router.get('/commit-diff', async (req, res) => {
|
||||
const { project, commit } = req.query;
|
||||
|
||||
if (!project || !commit) {
|
||||
return res.status(400).json({ error: 'Project name and commit hash are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get diff for the commit
|
||||
const { stdout } = await execAsync(
|
||||
`git show ${commit}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
res.json({ diff: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git commit diff error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate commit message based on staged changes
|
||||
router.post('/generate-commit-message', async (req, res) => {
|
||||
const { project, files } = req.body;
|
||||
|
||||
if (!project || !files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'Project name and files are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get diff for selected files
|
||||
let combinedDiff = '';
|
||||
for (const file of files) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git diff HEAD -- "${file}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
if (stdout) {
|
||||
combinedDiff += `\n--- ${file} ---\n${stdout}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting diff for ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Use AI to generate commit message (simple implementation)
|
||||
// In a real implementation, you might want to use GPT or Claude API
|
||||
const message = generateSimpleCommitMessage(files, combinedDiff);
|
||||
|
||||
res.json({ message });
|
||||
} catch (error) {
|
||||
console.error('Generate commit message error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Simple commit message generator (can be replaced with AI)
|
||||
function generateSimpleCommitMessage(files, diff) {
|
||||
const fileCount = files.length;
|
||||
const isMultipleFiles = fileCount > 1;
|
||||
|
||||
// Analyze the diff to determine the type of change
|
||||
const additions = (diff.match(/^\+[^+]/gm) || []).length;
|
||||
const deletions = (diff.match(/^-[^-]/gm) || []).length;
|
||||
|
||||
// Determine the primary action
|
||||
let action = 'Update';
|
||||
if (additions > 0 && deletions === 0) {
|
||||
action = 'Add';
|
||||
} else if (deletions > 0 && additions === 0) {
|
||||
action = 'Remove';
|
||||
} else if (additions > deletions * 2) {
|
||||
action = 'Enhance';
|
||||
} else if (deletions > additions * 2) {
|
||||
action = 'Refactor';
|
||||
}
|
||||
|
||||
// Generate message based on files
|
||||
if (isMultipleFiles) {
|
||||
const components = new Set(files.map(f => {
|
||||
const parts = f.split('/');
|
||||
return parts[parts.length - 2] || parts[0];
|
||||
}));
|
||||
|
||||
if (components.size === 1) {
|
||||
return `${action} ${[...components][0]} component`;
|
||||
} else {
|
||||
return `${action} multiple components`;
|
||||
}
|
||||
} else {
|
||||
const fileName = files[0].split('/').pop();
|
||||
const componentName = fileName.replace(/\.(jsx?|tsx?|css|scss)$/, '');
|
||||
return `${action} ${componentName}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
70
src/App.jsx
Normal file → Executable file
@@ -24,7 +24,11 @@ import Sidebar from './components/Sidebar';
|
||||
import MainContent from './components/MainContent';
|
||||
import MobileNav from './components/MobileNav';
|
||||
import ToolsSettings from './components/ToolsSettings';
|
||||
import QuickSettingsPanel from './components/QuickSettingsPanel';
|
||||
|
||||
import { useWebSocket } from './utils/websocket';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
|
||||
|
||||
// Main App component with routing
|
||||
function AppContent() {
|
||||
@@ -40,6 +44,19 @@ function AppContent() {
|
||||
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [showToolsSettings, setShowToolsSettings] = useState(false);
|
||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||
const [autoExpandTools, setAutoExpandTools] = useState(() => {
|
||||
const saved = localStorage.getItem('autoExpandTools');
|
||||
return saved !== null ? JSON.parse(saved) : false;
|
||||
});
|
||||
const [showRawParameters, setShowRawParameters] = useState(() => {
|
||||
const saved = localStorage.getItem('showRawParameters');
|
||||
return saved !== null ? JSON.parse(saved) : false;
|
||||
});
|
||||
const [autoScrollToBottom, setAutoScrollToBottom] = useState(() => {
|
||||
const saved = localStorage.getItem('autoScrollToBottom');
|
||||
return saved !== null ? JSON.parse(saved) : true;
|
||||
});
|
||||
// Session Protection System: Track sessions with active conversations to prevent
|
||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||
// a message, the session is marked as "active" and project updates are paused
|
||||
@@ -204,13 +221,18 @@ function AppContent() {
|
||||
// Handle URL-based session loading
|
||||
useEffect(() => {
|
||||
if (sessionId && projects.length > 0) {
|
||||
// Only switch tabs on initial load, not on every project update
|
||||
const shouldSwitchTab = !selectedSession || selectedSession.id !== sessionId;
|
||||
// Find the session across all projects
|
||||
for (const project of projects) {
|
||||
const session = project.sessions?.find(s => s.id === sessionId);
|
||||
if (session) {
|
||||
setSelectedProject(project);
|
||||
setSelectedSession(session);
|
||||
setActiveTab('chat');
|
||||
// Only switch to chat tab if we're loading a different session
|
||||
if (shouldSwitchTab) {
|
||||
setActiveTab('chat');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +254,11 @@ function AppContent() {
|
||||
|
||||
const handleSessionSelect = (session) => {
|
||||
setSelectedSession(session);
|
||||
setActiveTab('chat');
|
||||
// Only switch to chat tab when user explicitly selects a session
|
||||
// This prevents tab switching during automatic updates
|
||||
if (activeTab !== 'git' && activeTab !== 'preview') {
|
||||
setActiveTab('chat');
|
||||
}
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
@@ -455,6 +481,9 @@ function AppContent() {
|
||||
onSessionInactive={markSessionAsInactive}
|
||||
onReplaceTemporarySession={replaceTemporarySession}
|
||||
onNavigateToSession={(sessionId) => navigate(`/session/${sessionId}`)}
|
||||
onShowSettings={() => setShowToolsSettings(true)}
|
||||
autoExpandTools={autoExpandTools}
|
||||
showRawParameters={showRawParameters}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -466,6 +495,29 @@ function AppContent() {
|
||||
isInputFocused={isInputFocused}
|
||||
/>
|
||||
)}
|
||||
{/* Quick Settings Panel - Only show on chat tab */}
|
||||
{activeTab === 'chat' && (
|
||||
<QuickSettingsPanel
|
||||
isOpen={showQuickSettings}
|
||||
onToggle={setShowQuickSettings}
|
||||
autoExpandTools={autoExpandTools}
|
||||
onAutoExpandChange={(value) => {
|
||||
setAutoExpandTools(value);
|
||||
localStorage.setItem('autoExpandTools', JSON.stringify(value));
|
||||
}}
|
||||
showRawParameters={showRawParameters}
|
||||
onShowRawParametersChange={(value) => {
|
||||
setShowRawParameters(value);
|
||||
localStorage.setItem('showRawParameters', JSON.stringify(value));
|
||||
}}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
onAutoScrollChange={(value) => {
|
||||
setAutoScrollToBottom(value);
|
||||
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
||||
}}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tools Settings Modal */}
|
||||
<ToolsSettings
|
||||
@@ -479,12 +531,14 @@ function AppContent() {
|
||||
// Root App component with router
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<ThemeProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<AppContent />} />
|
||||
<Route path="/session/:sessionId" element={<AppContent />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
782
src/components/ChatInterface.jsx
Normal file → Executable file
11
src/components/ClaudeLogo.jsx
Executable file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const ClaudeLogo = ({className = 'w-5 h-5'}) => {
|
||||
return (
|
||||
<img src="/icons/claude-ai-icon.svg" alt="Claude" className={className} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaudeLogo;
|
||||
|
||||
|
||||
107
src/components/ClaudeStatus.jsx
Executable file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
function ClaudeStatus({ status, onAbort, isLoading }) {
|
||||
const [elapsedTime, setElapsedTime] = useState(0);
|
||||
const [animationPhase, setAnimationPhase] = useState(0);
|
||||
const [fakeTokens, setFakeTokens] = useState(0);
|
||||
|
||||
// Update elapsed time every second
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setElapsedTime(0);
|
||||
setFakeTokens(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const timer = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
setElapsedTime(elapsed);
|
||||
// Simulate token count increasing over time (roughly 30-50 tokens per second)
|
||||
setFakeTokens(Math.floor(elapsed * (30 + Math.random() * 20)));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
// Animate the status indicator
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setAnimationPhase(prev => (prev + 1) % 4);
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
if (!isLoading) return null;
|
||||
|
||||
// Clever action words that cycle
|
||||
const actionWords = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning'];
|
||||
const actionIndex = Math.floor(elapsedTime / 3) % actionWords.length;
|
||||
|
||||
// Parse status data
|
||||
const statusText = status?.text || actionWords[actionIndex];
|
||||
const tokens = status?.tokens || fakeTokens;
|
||||
const canInterrupt = status?.can_interrupt !== false;
|
||||
|
||||
// Animation characters
|
||||
const spinners = ['✻', '✹', '✸', '✶'];
|
||||
const currentSpinner = spinners[animationPhase];
|
||||
|
||||
return (
|
||||
<div className="w-full mb-6 animate-in slide-in-from-bottom duration-300">
|
||||
<div className="flex items-center justify-between max-w-4xl mx-auto bg-gray-900 dark:bg-gray-950 text-white rounded-lg shadow-lg px-4 py-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Animated spinner */}
|
||||
<span className={cn(
|
||||
"text-xl transition-all duration-500",
|
||||
animationPhase % 2 === 0 ? "text-blue-400 scale-110" : "text-blue-300"
|
||||
)}>
|
||||
{currentSpinner}
|
||||
</span>
|
||||
|
||||
{/* Status text - first line */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{statusText}...</span>
|
||||
<span className="text-gray-400 text-sm">({elapsedTime}s)</span>
|
||||
{tokens > 0 && (
|
||||
<>
|
||||
<span className="text-gray-400">·</span>
|
||||
<span className="text-gray-300 text-sm hidden sm:inline">⚒ {tokens.toLocaleString()} tokens</span>
|
||||
<span className="text-gray-300 text-sm sm:hidden">⚒ {tokens.toLocaleString()}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-gray-400 hidden sm:inline">·</span>
|
||||
<span className="text-gray-300 text-sm hidden sm:inline">esc to interrupt</span>
|
||||
</div>
|
||||
{/* Second line for mobile */}
|
||||
<div className="text-xs text-gray-400 sm:hidden mt-1">
|
||||
esc to interrupt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interrupt button */}
|
||||
{canInterrupt && onAbort && (
|
||||
<button
|
||||
onClick={onAbort}
|
||||
className="ml-3 text-xs bg-red-600 hover:bg-red-700 text-white px-2.5 py-1 sm:px-3 sm:py-1.5 rounded-md transition-colors flex items-center gap-1.5 flex-shrink-0"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Stop</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaudeStatus;
|
||||
0
src/components/CodeEditor.jsx
Normal file → Executable file
35
src/components/DarkModeToggle.jsx
Executable file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
function DarkModeToggle() {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={isDarkMode}
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<span className="sr-only">Toggle dark mode</span>
|
||||
<span
|
||||
className={`${
|
||||
isDarkMode ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<svg className="w-3.5 h-3.5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3.5 h-3.5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default DarkModeToggle;
|
||||
0
src/components/FileTree.jsx
Normal file → Executable file
777
src/components/GitPanel.jsx
Executable file
@@ -0,0 +1,777 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles } from 'lucide-react';
|
||||
import { MicButton } from './MicButton.jsx';
|
||||
|
||||
function GitPanel({ selectedProject, isMobile }) {
|
||||
const [gitStatus, setGitStatus] = useState(null);
|
||||
const [gitDiff, setGitDiff] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [commitMessage, setCommitMessage] = useState('');
|
||||
const [expandedFiles, setExpandedFiles] = useState(new Set());
|
||||
const [selectedFiles, setSelectedFiles] = useState(new Set());
|
||||
const [isCommitting, setIsCommitting] = useState(false);
|
||||
const [currentBranch, setCurrentBranch] = useState('');
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [wrapText, setWrapText] = useState(true);
|
||||
const [showLegend, setShowLegend] = useState(false);
|
||||
const [showBranchDropdown, setShowBranchDropdown] = useState(false);
|
||||
const [showNewBranchModal, setShowNewBranchModal] = useState(false);
|
||||
const [newBranchName, setNewBranchName] = useState('');
|
||||
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
|
||||
const [activeView, setActiveView] = useState('changes'); // 'changes' or 'history'
|
||||
const [recentCommits, setRecentCommits] = useState([]);
|
||||
const [expandedCommits, setExpandedCommits] = useState(new Set());
|
||||
const [commitDiffs, setCommitDiffs] = useState({});
|
||||
const [isGeneratingMessage, setIsGeneratingMessage] = useState(false);
|
||||
const textareaRef = useRef(null);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject) {
|
||||
fetchGitStatus();
|
||||
fetchBranches();
|
||||
if (activeView === 'history') {
|
||||
fetchRecentCommits();
|
||||
}
|
||||
}
|
||||
}, [selectedProject, activeView]);
|
||||
|
||||
// Handle click outside dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setShowBranchDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const fetchGitStatus = async () => {
|
||||
if (!selectedProject) return;
|
||||
|
||||
console.log('Fetching git status for project:', selectedProject.name, 'path:', selectedProject.path);
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Git status response:', data);
|
||||
|
||||
if (data.error) {
|
||||
console.error('Git status error:', data.error);
|
||||
setGitStatus(null);
|
||||
} else {
|
||||
setGitStatus(data);
|
||||
setCurrentBranch(data.branch || 'main');
|
||||
|
||||
// Auto-select all changed files
|
||||
const allFiles = new Set([
|
||||
...(data.modified || []),
|
||||
...(data.added || []),
|
||||
...(data.deleted || []),
|
||||
...(data.untracked || [])
|
||||
]);
|
||||
setSelectedFiles(allFiles);
|
||||
|
||||
// Fetch diffs for changed files
|
||||
for (const file of data.modified || []) {
|
||||
fetchFileDiff(file);
|
||||
}
|
||||
for (const file of data.added || []) {
|
||||
fetchFileDiff(file);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching git status:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/branches?project=${encodeURIComponent(selectedProject.name)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.branches) {
|
||||
setBranches(data.branches);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const switchBranch = async (branchName) => {
|
||||
try {
|
||||
const response = await fetch('/api/git/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
branch: branchName
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setCurrentBranch(branchName);
|
||||
setShowBranchDropdown(false);
|
||||
fetchGitStatus(); // Refresh status after branch switch
|
||||
} else {
|
||||
console.error('Failed to switch branch:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching branch:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const createBranch = async () => {
|
||||
if (!newBranchName.trim()) return;
|
||||
|
||||
setIsCreatingBranch(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/create-branch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
branch: newBranchName.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setCurrentBranch(newBranchName.trim());
|
||||
setShowNewBranchModal(false);
|
||||
setShowBranchDropdown(false);
|
||||
setNewBranchName('');
|
||||
fetchBranches(); // Refresh branch list
|
||||
fetchGitStatus(); // Refresh status
|
||||
} else {
|
||||
console.error('Failed to create branch:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating branch:', error);
|
||||
} finally {
|
||||
setIsCreatingBranch(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileDiff = async (filePath) => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
setGitDiff(prev => ({
|
||||
...prev,
|
||||
[filePath]: data.diff
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching file diff:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecentCommits = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.commits) {
|
||||
setRecentCommits(data.commits);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching commits:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommitDiff = async (commitHash) => {
|
||||
try {
|
||||
const response = await fetch(`/api/git/commit-diff?project=${encodeURIComponent(selectedProject.name)}&commit=${commitHash}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error && data.diff) {
|
||||
setCommitDiffs(prev => ({
|
||||
...prev,
|
||||
[commitHash]: data.diff
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching commit diff:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const generateCommitMessage = async () => {
|
||||
setIsGeneratingMessage(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/generate-commit-message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
files: Array.from(selectedFiles)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.message) {
|
||||
setCommitMessage(data.message);
|
||||
} else {
|
||||
console.error('Failed to generate commit message:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating commit message:', error);
|
||||
} finally {
|
||||
setIsGeneratingMessage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFileExpanded = (filePath) => {
|
||||
setExpandedFiles(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(filePath)) {
|
||||
newSet.delete(filePath);
|
||||
} else {
|
||||
newSet.add(filePath);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleCommitExpanded = (commitHash) => {
|
||||
setExpandedCommits(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(commitHash)) {
|
||||
newSet.delete(commitHash);
|
||||
} else {
|
||||
newSet.add(commitHash);
|
||||
// Fetch diff for this commit if not already fetched
|
||||
if (!commitDiffs[commitHash]) {
|
||||
fetchCommitDiff(commitHash);
|
||||
}
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFileSelected = (filePath) => {
|
||||
setSelectedFiles(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(filePath)) {
|
||||
newSet.delete(filePath);
|
||||
} else {
|
||||
newSet.add(filePath);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCommit = async () => {
|
||||
if (!commitMessage.trim() || selectedFiles.size === 0) return;
|
||||
|
||||
setIsCommitting(true);
|
||||
try {
|
||||
const response = await fetch('/api/git/commit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project: selectedProject.name,
|
||||
message: commitMessage,
|
||||
files: Array.from(selectedFiles)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Reset state after successful commit
|
||||
setCommitMessage('');
|
||||
setSelectedFiles(new Set());
|
||||
fetchGitStatus();
|
||||
} else {
|
||||
console.error('Commit failed:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error committing changes:', error);
|
||||
} finally {
|
||||
setIsCommitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDiffLine = (line, index) => {
|
||||
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
||||
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
||||
const isHeader = line.startsWith('@@');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`font-mono text-xs ${
|
||||
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
|
||||
} ${
|
||||
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
|
||||
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
|
||||
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
|
||||
'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
switch (status) {
|
||||
case 'M': return 'Modified';
|
||||
case 'A': return 'Added';
|
||||
case 'D': return 'Deleted';
|
||||
case 'U': return 'Untracked';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCommitItem = (commit) => {
|
||||
const isExpanded = expandedCommits.has(commit.hash);
|
||||
const diff = commitDiffs[commit.hash];
|
||||
|
||||
return (
|
||||
<div key={commit.hash} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
||||
<div
|
||||
className="flex items-start p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
onClick={() => toggleCommitExpanded(commit.hash)}
|
||||
>
|
||||
<div className="mr-2 mt-1 p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
||||
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{commit.message}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{commit.author} • {commit.date}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
|
||||
{commit.hash.substring(0, 7)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && diff && (
|
||||
<div className="bg-gray-50 dark:bg-gray-900">
|
||||
<div className="max-h-96 overflow-y-auto p-2">
|
||||
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
|
||||
{commit.stats}
|
||||
</div>
|
||||
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileItem = (filePath, status) => {
|
||||
const isExpanded = expandedFiles.has(filePath);
|
||||
const isSelected = selectedFiles.has(filePath);
|
||||
const diff = gitDiff[filePath];
|
||||
|
||||
return (
|
||||
<div key={filePath} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
||||
<div className="flex items-center px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFileSelected(filePath)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mr-2 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div
|
||||
className="flex items-center flex-1 cursor-pointer"
|
||||
onClick={() => toggleFileExpanded(filePath)}
|
||||
>
|
||||
<div className="mr-2 p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
||||
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||
</div>
|
||||
<span className="flex-1 text-sm truncate">{filePath}</span>
|
||||
<span
|
||||
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
||||
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
||||
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 border-green-200 dark:border-green-800' :
|
||||
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 border-red-200 dark:border-red-800' :
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
title={getStatusLabel(status)}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && diff && (
|
||||
<div className="bg-gray-50 dark:bg-gray-900">
|
||||
{isMobile && (
|
||||
<div className="flex justify-end p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setWrapText(!wrapText);
|
||||
}}
|
||||
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
title={wrapText ? "Switch to horizontal scroll" : "Switch to text wrap"}
|
||||
>
|
||||
{wrapText ? '↔️ Scroll' : '↩️ Wrap'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-96 overflow-y-auto p-2">
|
||||
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
<p>Select a project to view source control</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-white dark:bg-gray-900">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowBranchDropdown(!showBranchDropdown)}
|
||||
className="flex items-center space-x-2 px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium">{currentBranch}</span>
|
||||
<ChevronDown className={`w-3 h-3 text-gray-500 transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Branch Dropdown */}
|
||||
{showBranchDropdown && (
|
||||
<div className="absolute top-full left-0 mt-1 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div className="py-1 max-h-64 overflow-y-auto">
|
||||
{branches.map(branch => (
|
||||
<button
|
||||
key={branch}
|
||||
onClick={() => switchBranch(branch)}
|
||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||
branch === currentBranch ? 'bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{branch === currentBranch && <Check className="w-3 h-3 text-green-600 dark:text-green-400" />}
|
||||
<span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNewBranchModal(true);
|
||||
setShowBranchDropdown(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
<span>Create new branch</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
fetchGitStatus();
|
||||
fetchBranches();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setActiveView('changes')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeView === 'changes'
|
||||
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Changes</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView('history')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeView === 'history'
|
||||
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<History className="w-4 h-4" />
|
||||
<span>History</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Changes View */}
|
||||
{activeView === 'changes' && (
|
||||
<>
|
||||
{/* Commit Message Input */}
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
placeholder="Message (Ctrl+Enter to commit)"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 resize-none pr-20"
|
||||
rows="3"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
handleCommit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-2 top-2 flex gap-1">
|
||||
<button
|
||||
onClick={generateCommitMessage}
|
||||
disabled={selectedFiles.size === 0 || isGeneratingMessage}
|
||||
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Generate commit message"
|
||||
>
|
||||
{isGeneratingMessage ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<MicButton
|
||||
onTranscript={(transcript) => setCommitMessage(transcript)}
|
||||
mode="default"
|
||||
className="p-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCommit}
|
||||
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
<span>{isCommitting ? 'Committing...' : 'Commit'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* File Selection Controls - Only show in changes view */}
|
||||
{activeView === 'changes' && gitStatus && (
|
||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{selectedFiles.size} of {(gitStatus?.modified?.length || 0) + (gitStatus?.added?.length || 0) + (gitStatus?.deleted?.length || 0) + (gitStatus?.untracked?.length || 0)} files selected
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const allFiles = new Set([
|
||||
...(gitStatus?.modified || []),
|
||||
...(gitStatus?.added || []),
|
||||
...(gitStatus?.deleted || []),
|
||||
...(gitStatus?.untracked || [])
|
||||
]);
|
||||
setSelectedFiles(allFiles);
|
||||
}}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
||||
<button
|
||||
onClick={() => setSelectedFiles(new Set())}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
>
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Legend Toggle */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setShowLegend(!showLegend)}
|
||||
className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 text-xs text-gray-600 dark:text-gray-400 flex items-center justify-center gap-1"
|
||||
>
|
||||
<Info className="w-3 h-3" />
|
||||
<span>File Status Guide</span>
|
||||
{showLegend ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||
</button>
|
||||
|
||||
{showLegend && (
|
||||
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-800 text-xs">
|
||||
<div className={`${isMobile ? 'grid grid-cols-2 gap-3 justify-items-center' : 'flex justify-center gap-6'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 rounded border border-yellow-200 dark:border-yellow-800 font-bold text-xs">
|
||||
M
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 italic">Modified</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 rounded border border-green-200 dark:border-green-800 font-bold text-xs">
|
||||
A
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 italic">Added</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 rounded border border-red-200 dark:border-red-800 font-bold text-xs">
|
||||
D
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 italic">Deleted</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 rounded border border-gray-300 dark:border-gray-600 font-bold text-xs">
|
||||
U
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-400 italic">Untracked</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File List - Changes View */}
|
||||
{activeView === 'changes' && (
|
||||
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-20' : ''}`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : !gitStatus || (!gitStatus.modified?.length && !gitStatus.added?.length && !gitStatus.deleted?.length && !gitStatus.untracked?.length) ? (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
||||
<GitCommit className="w-12 h-12 mb-2 opacity-50" />
|
||||
<p className="text-sm">No changes detected</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={isMobile ? 'pb-4' : ''}>
|
||||
{gitStatus.modified?.map(file => renderFileItem(file, 'M'))}
|
||||
{gitStatus.added?.map(file => renderFileItem(file, 'A'))}
|
||||
{gitStatus.deleted?.map(file => renderFileItem(file, 'D'))}
|
||||
{gitStatus.untracked?.map(file => renderFileItem(file, 'U'))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History View */}
|
||||
{activeView === 'history' && (
|
||||
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-20' : ''}`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : recentCommits.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
||||
<History className="w-12 h-12 mb-2 opacity-50" />
|
||||
<p className="text-sm">No commits found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={isMobile ? 'pb-4' : ''}>
|
||||
{recentCommits.map(commit => renderCommitItem(commit))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New Branch Modal */}
|
||||
{showNewBranchModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowNewBranchModal(false)} />
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Create New Branch</h3>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Branch Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newBranchName}
|
||||
onChange={(e) => setNewBranchName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isCreatingBranch) {
|
||||
createBranch();
|
||||
}
|
||||
}}
|
||||
placeholder="feature/new-feature"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
||||
This will create a new branch from the current branch ({currentBranch})
|
||||
</div>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNewBranchModal(false);
|
||||
setNewBranchName('');
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={createBranch}
|
||||
disabled={!newBranchName.trim() || isCreatingBranch}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||
>
|
||||
{isCreatingBranch ? (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
<span>Creating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-3 h-3" />
|
||||
<span>Create Branch</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitPanel;
|
||||
0
src/components/ImageViewer.jsx
Normal file → Executable file
89
src/components/MainContent.jsx
Normal file → Executable file
@@ -11,11 +11,12 @@
|
||||
* No session protection logic is implemented here - it's purely a props bridge.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ChatInterface from './ChatInterface';
|
||||
import FileTree from './FileTree';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import Shell from './Shell';
|
||||
import GitPanel from './GitPanel';
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
@@ -34,7 +35,11 @@ function MainContent({
|
||||
onSessionActive, // Mark session as active when user sends message
|
||||
onSessionInactive, // Mark session as inactive when conversation completes/aborts
|
||||
onReplaceTemporarySession, // Replace temporary session ID with real session ID from WebSocket
|
||||
onNavigateToSession // Navigate to a specific session (for Claude CLI session duplication workaround)
|
||||
onNavigateToSession, // Navigate to a specific session (for Claude CLI session duplication workaround)
|
||||
onShowSettings, // Show tools settings panel
|
||||
autoExpandTools, // Auto-expand tool accordions
|
||||
showRawParameters, // Show raw parameters in tool accordions
|
||||
autoScrollToBottom // Auto-scroll to bottom when new messages arrive
|
||||
}) {
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
|
||||
@@ -70,8 +75,15 @@ function MainContent({
|
||||
)}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||
<div className="w-16 h-16 mx-auto mb-4">
|
||||
<div className="w-16 h-16 animate-spin rounded-full border-4 border-gray-300 dark:border-gray-600 border-t-blue-600" />
|
||||
<div className="w-12 h-12 mx-auto mb-4">
|
||||
<div
|
||||
className="w-full h-full rounded-full border-4 border-gray-200 border-t-blue-500"
|
||||
style={{
|
||||
animation: 'spin 1s linear infinite',
|
||||
WebkitAnimation: 'spin 1s linear infinite',
|
||||
MozAnimation: 'spin 1s linear infinite'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Loading Claude Code UI</h2>
|
||||
<p>Setting up your workspace...</p>
|
||||
@@ -132,7 +144,7 @@ function MainContent({
|
||||
e.preventDefault();
|
||||
onMenuClick();
|
||||
}}
|
||||
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 transition-transform duration-75"
|
||||
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
@@ -161,7 +173,7 @@ function MainContent({
|
||||
) : (
|
||||
<div>
|
||||
<h2 className="text-base sm:text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Project Files
|
||||
{activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
|
||||
</h2>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{selectedProject.displayName}
|
||||
@@ -176,7 +188,7 @@ function MainContent({
|
||||
<div className="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md ${
|
||||
activeTab === 'chat'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
@@ -219,6 +231,36 @@ function MainContent({
|
||||
<span className="hidden sm:inline">Files</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('git')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'git'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Source Control</span>
|
||||
</span>
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={() => setActiveTab('preview')}
|
||||
className={`relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'preview'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Preview</span>
|
||||
</span>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,6 +281,10 @@ function MainContent({
|
||||
onSessionInactive={onSessionInactive}
|
||||
onReplaceTemporarySession={onReplaceTemporarySession}
|
||||
onNavigateToSession={onNavigateToSession}
|
||||
onShowSettings={onShowSettings}
|
||||
autoExpandTools={autoExpandTools}
|
||||
showRawParameters={showRawParameters}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>
|
||||
@@ -251,6 +297,35 @@ function MainContent({
|
||||
isActive={activeTab === 'shell'}
|
||||
/>
|
||||
</div>
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'git' ? 'block' : 'hidden'}`}>
|
||||
<GitPanel selectedProject={selectedProject} isMobile={isMobile} />
|
||||
</div>
|
||||
<div className={`h-full overflow-hidden ${activeTab === 'preview' ? 'block' : 'hidden'}`}>
|
||||
{/* <LivePreviewPanel
|
||||
selectedProject={selectedProject}
|
||||
serverStatus={serverStatus}
|
||||
serverUrl={serverUrl}
|
||||
availableScripts={availableScripts}
|
||||
onStartServer={(script) => {
|
||||
sendMessage({
|
||||
type: 'server:start',
|
||||
projectPath: selectedProject?.fullPath,
|
||||
script: script
|
||||
});
|
||||
}}
|
||||
onStopServer={() => {
|
||||
sendMessage({
|
||||
type: 'server:stop',
|
||||
projectPath: selectedProject?.fullPath
|
||||
});
|
||||
}}
|
||||
onScriptSelect={setCurrentScript}
|
||||
currentScript={currentScript}
|
||||
isMobile={isMobile}
|
||||
serverLogs={serverLogs}
|
||||
onClearLogs={() => setServerLogs([])}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Editor Modal */}
|
||||
|
||||
216
src/components/MicButton.jsx
Executable file
@@ -0,0 +1,216 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Mic, Loader2, Brain } from 'lucide-react';
|
||||
import { transcribeWithWhisper } from '../utils/whisper';
|
||||
|
||||
export function MicButton({ onTranscript, className = '' }) {
|
||||
const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const streamRef = useRef(null);
|
||||
const chunksRef = useRef([]);
|
||||
const lastTapRef = useRef(0);
|
||||
|
||||
// Version indicator to verify updates
|
||||
|
||||
// Start recording
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
console.log('Starting recording...');
|
||||
setError(null);
|
||||
chunksRef.current = [];
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
streamRef.current = stream;
|
||||
|
||||
const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
|
||||
const recorder = new MediaRecorder(stream, { mimeType });
|
||||
mediaRecorderRef.current = recorder;
|
||||
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
chunksRef.current.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = async () => {
|
||||
console.log('Recording stopped, creating blob...');
|
||||
const blob = new Blob(chunksRef.current, { type: mimeType });
|
||||
|
||||
// Clean up stream
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach(track => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
|
||||
// Start transcribing
|
||||
setState('transcribing');
|
||||
|
||||
// Check if we're in an enhancement mode
|
||||
const whisperMode = window.localStorage.getItem('whisperMode') || 'default';
|
||||
const isEnhancementMode = whisperMode === 'prompt' || whisperMode === 'vibe' || whisperMode === 'instructions' || whisperMode === 'architect';
|
||||
|
||||
// Set up a timer to switch to processing state for enhancement modes
|
||||
let processingTimer;
|
||||
if (isEnhancementMode) {
|
||||
processingTimer = setTimeout(() => {
|
||||
setState('processing');
|
||||
}, 2000); // Switch to processing after 2 seconds
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await transcribeWithWhisper(blob);
|
||||
if (text && onTranscript) {
|
||||
onTranscript(text);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Transcription error:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
if (processingTimer) {
|
||||
clearTimeout(processingTimer);
|
||||
}
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
setState('recording');
|
||||
console.log('Recording started successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to start recording:', err);
|
||||
setError('Microphone access denied');
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
// Stop recording
|
||||
const stopRecording = () => {
|
||||
console.log('Stopping recording...');
|
||||
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
|
||||
mediaRecorderRef.current.stop();
|
||||
// Don't set state here - let the onstop handler do it
|
||||
} else {
|
||||
// If recorder isn't in recording state, force cleanup
|
||||
console.log('Recorder not in recording state, forcing cleanup');
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach(track => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle button click
|
||||
const handleClick = (e) => {
|
||||
// Prevent double firing on mobile
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Debounce for mobile double-tap issue
|
||||
const now = Date.now();
|
||||
if (now - lastTapRef.current < 300) {
|
||||
console.log('Ignoring rapid tap');
|
||||
return;
|
||||
}
|
||||
lastTapRef.current = now;
|
||||
|
||||
console.log('Button clicked, current state:', state);
|
||||
|
||||
if (state === 'idle') {
|
||||
startRecording();
|
||||
} else if (state === 'recording') {
|
||||
stopRecording();
|
||||
}
|
||||
// Do nothing if transcribing or processing
|
||||
};
|
||||
|
||||
// Clean up on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Button appearance based on state
|
||||
const getButtonAppearance = () => {
|
||||
switch (state) {
|
||||
case 'recording':
|
||||
return {
|
||||
icon: <Mic className="w-5 h-5 text-white" />,
|
||||
className: 'bg-red-500 hover:bg-red-600 animate-pulse',
|
||||
disabled: false
|
||||
};
|
||||
case 'transcribing':
|
||||
return {
|
||||
icon: <Loader2 className="w-5 h-5 animate-spin" />,
|
||||
className: 'bg-blue-500 hover:bg-blue-600',
|
||||
disabled: true
|
||||
};
|
||||
case 'processing':
|
||||
return {
|
||||
icon: <Brain className="w-5 h-5 animate-pulse" />,
|
||||
className: 'bg-purple-500 hover:bg-purple-600',
|
||||
disabled: true
|
||||
};
|
||||
default: // idle
|
||||
return {
|
||||
icon: <Mic className="w-5 h-5" />,
|
||||
className: 'bg-gray-700 hover:bg-gray-600',
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, className: buttonClass, disabled } = getButtonAppearance();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
backgroundColor: state === 'recording' ? '#ef4444' :
|
||||
state === 'transcribing' ? '#3b82f6' :
|
||||
state === 'processing' ? '#a855f7' :
|
||||
'#374151'
|
||||
}}
|
||||
className={`
|
||||
flex items-center justify-center
|
||||
w-12 h-12 rounded-full
|
||||
text-white transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
|
||||
dark:ring-offset-gray-800
|
||||
touch-action-manipulation
|
||||
${disabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
|
||||
${state === 'recording' ? 'animate-pulse' : ''}
|
||||
hover:opacity-90
|
||||
${className}
|
||||
`}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2
|
||||
bg-red-500 text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10
|
||||
animate-fade-in">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'recording' && (
|
||||
<div className="absolute -inset-1 rounded-full border-2 border-red-500 animate-ping pointer-events-none" />
|
||||
)}
|
||||
|
||||
{state === 'processing' && (
|
||||
<div className="absolute -inset-1 rounded-full border-2 border-purple-500 animate-ping pointer-events-none" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
src/components/MobileNav.jsx
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { MessageSquare, Folder, Terminal } from 'lucide-react';
|
||||
import { MessageSquare, Folder, Terminal, GitBranch, Globe } from 'lucide-react';
|
||||
|
||||
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||
// Detect dark mode
|
||||
@@ -19,6 +19,11 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||
id: 'files',
|
||||
icon: Folder,
|
||||
onClick: () => setActiveTab('files')
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
icon: GitBranch,
|
||||
onClick: () => setActiveTab('git')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -52,7 +57,7 @@ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
}}
|
||||
className={`flex items-center justify-center p-2 rounded-lg transition-colors duration-75 min-h-[40px] min-w-[40px] relative touch-manipulation ${
|
||||
className={`flex items-center justify-center p-2 rounded-lg min-h-[40px] min-w-[40px] relative touch-manipulation ${
|
||||
isActive
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
|
||||
238
src/components/QuickSettingsPanel.jsx
Executable file
@@ -0,0 +1,238 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Maximize2,
|
||||
Eye,
|
||||
Settings2,
|
||||
Moon,
|
||||
Sun,
|
||||
ArrowDown,
|
||||
Mic,
|
||||
Brain,
|
||||
Sparkles,
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
import DarkModeToggle from './DarkModeToggle';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const QuickSettingsPanel = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
autoExpandTools,
|
||||
onAutoExpandChange,
|
||||
showRawParameters,
|
||||
onShowRawParametersChange,
|
||||
autoScrollToBottom,
|
||||
onAutoScrollChange,
|
||||
isMobile
|
||||
}) => {
|
||||
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
||||
const [whisperMode, setWhisperMode] = useState(() => {
|
||||
return localStorage.getItem('whisperMode') || 'default';
|
||||
});
|
||||
const { isDarkMode } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setLocalIsOpen(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleToggle = () => {
|
||||
const newState = !localIsOpen;
|
||||
setLocalIsOpen(newState);
|
||||
onToggle(newState);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Pull Tab */}
|
||||
<div
|
||||
className={`fixed ${isMobile ? 'bottom-44' : 'top-1/2 -translate-y-1/2'} ${
|
||||
localIsOpen ? 'right-64' : 'right-0'
|
||||
} z-50 transition-all duration-150 ease-out`}
|
||||
>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-l-md p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors shadow-lg"
|
||||
aria-label={localIsOpen ? 'Close settings panel' : 'Open settings panel'}
|
||||
>
|
||||
{localIsOpen ? (
|
||||
<ChevronRight className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
) : (
|
||||
<ChevronLeft className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Panel */}
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full w-64 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-700 shadow-xl transform transition-transform duration-150 ease-out z-40 ${
|
||||
localIsOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
} ${isMobile ? 'h-screen' : ''}`}
|
||||
>
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||
Quick Settings
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className={`flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 bg-white dark:bg-gray-900 ${isMobile ? 'pb-20' : ''}`}>
|
||||
{/* Appearance Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Appearance</h4>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
{isDarkMode ? <Moon className="h-4 w-4 text-gray-600 dark:text-gray-400" /> : <Sun className="h-4 w-4 text-gray-600 dark:text-gray-400" />}
|
||||
Dark Mode
|
||||
</span>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool Display Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Tool Display</h4>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Maximize2 className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Auto-expand tools
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoExpandTools}
|
||||
onChange={(e) => onAutoExpandChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<Eye className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Show raw parameters
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showRawParameters}
|
||||
onChange={(e) => onShowRawParametersChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* View Options */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">View Options</h4>
|
||||
|
||||
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||
<ArrowDown className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Auto-scroll to bottom
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScrollToBottom}
|
||||
onChange={(e) => onAutoScrollChange(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Whisper Dictation Settings */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-start p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<input
|
||||
type="radio"
|
||||
name="whisperMode"
|
||||
value="default"
|
||||
checked={whisperMode === 'default'}
|
||||
onChange={() => {
|
||||
setWhisperMode('default');
|
||||
localStorage.setItem('whisperMode', 'default');
|
||||
window.dispatchEvent(new Event('whisperModeChanged'));
|
||||
}}
|
||||
className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div className="ml-3 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<Mic className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Default Mode
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Direct transcription of your speech
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<input
|
||||
type="radio"
|
||||
name="whisperMode"
|
||||
value="prompt"
|
||||
checked={whisperMode === 'prompt'}
|
||||
onChange={() => {
|
||||
setWhisperMode('prompt');
|
||||
localStorage.setItem('whisperMode', 'prompt');
|
||||
window.dispatchEvent(new Event('whisperModeChanged'));
|
||||
}}
|
||||
className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div className="ml-3 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<Sparkles className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Prompt Enhancement
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Transform rough ideas into clear, detailed AI prompts
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||
<input
|
||||
type="radio"
|
||||
name="whisperMode"
|
||||
value="vibe"
|
||||
checked={whisperMode === 'vibe' || whisperMode === 'instructions' || whisperMode === 'architect'}
|
||||
onChange={() => {
|
||||
setWhisperMode('vibe');
|
||||
localStorage.setItem('whisperMode', 'vibe');
|
||||
window.dispatchEvent(new Event('whisperModeChanged'));
|
||||
}}
|
||||
className="mt-0.5 h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
||||
/>
|
||||
<div className="ml-3 flex-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
<FileText className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
Vibe Mode
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Format ideas as clear agent instructions with details
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backdrop */}
|
||||
{localIsOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-30 transition-opacity duration-150 ease-out"
|
||||
onClick={handleToggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSettingsPanel;
|
||||
16
src/components/Shell.jsx
Normal file → Executable file
@@ -493,11 +493,11 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
|
||||
{/* Connect button when not connected */}
|
||||
{isInitialized && !isConnected && !isConnecting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
|
||||
<div className="text-center">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
|
||||
<div className="text-center max-w-sm w-full">
|
||||
<button
|
||||
onClick={connectToShell}
|
||||
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center space-x-2 text-base font-medium"
|
||||
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center space-x-2 text-base font-medium w-full sm:w-auto"
|
||||
title="Connect to shell"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -505,7 +505,7 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
</svg>
|
||||
<span>Continue in Shell</span>
|
||||
</button>
|
||||
<p className="text-gray-400 text-sm mt-3">
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
{selectedSession ?
|
||||
`Resume session: ${selectedSession.summary.slice(0, 50)}...` :
|
||||
'Start a new Claude session'
|
||||
@@ -517,13 +517,13 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
||||
|
||||
{/* Connecting state */}
|
||||
{isConnecting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center space-x-3 text-yellow-400">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
|
||||
<div className="text-center max-w-sm w-full">
|
||||
<div className="flex items-center justify-center space-x-3 text-yellow-400">
|
||||
<div className="w-6 h-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
|
||||
<span className="text-base font-medium">Connecting to shell...</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mt-3">
|
||||
<p className="text-gray-400 text-sm mt-3 px-2">
|
||||
Starting Claude CLI in {selectedProject.displayName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
134
src/components/Sidebar.jsx
Normal file → Executable file
@@ -3,8 +3,9 @@ import { ScrollArea } from './ui/scroll-area';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw } from 'lucide-react';
|
||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2 } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime) => {
|
||||
@@ -56,6 +57,9 @@ function Sidebar({
|
||||
const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set());
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [editingSession, setEditingSession] = useState(null);
|
||||
const [editingSessionName, setEditingSessionName] = useState('');
|
||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||
|
||||
// Touch handler to prevent double-tap issues on iPad
|
||||
const handleTouchClick = (callback) => {
|
||||
@@ -601,7 +605,7 @@ function Sidebar({
|
||||
<>
|
||||
{getAllSessions(project).length === 0 && (
|
||||
<button
|
||||
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 transition-all duration-150 border border-red-200 dark:border-red-800"
|
||||
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteProject(project.name);
|
||||
@@ -612,7 +616,7 @@ function Sidebar({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 transition-all duration-150 border border-primary/20 dark:border-primary/30"
|
||||
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEditing(project);
|
||||
@@ -639,7 +643,7 @@ function Sidebar({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50 transition-colors duration-200",
|
||||
"hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50",
|
||||
isSelected && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
onClick={() => {
|
||||
@@ -781,14 +785,27 @@ function Sidebar({
|
||||
<p className="text-xs text-muted-foreground">No sessions yet</p>
|
||||
</div>
|
||||
) : (
|
||||
getAllSessions(project).map((session) => (
|
||||
getAllSessions(project).map((session) => {
|
||||
// Calculate if session is active (within last 10 minutes)
|
||||
const sessionDate = new Date(session.lastActivity);
|
||||
const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60));
|
||||
const isActive = diffInMinutes < 10;
|
||||
|
||||
return (
|
||||
<div key={session.id} className="group relative">
|
||||
{/* Active session indicator dot */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 -translate-x-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
{/* Mobile Session Item */}
|
||||
<div className="md:hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"p-2 mx-3 my-0.5 rounded-md bg-card border border-border/30 active:scale-[0.98] transition-all duration-150 relative",
|
||||
selectedSession?.id === session.id && "bg-primary/5 border-primary/20"
|
||||
"p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative",
|
||||
selectedSession?.id === session.id ? "bg-primary/5 border-primary/20" :
|
||||
isActive ? "border-green-500/30 bg-green-50/5 dark:bg-green-900/5" : "border-border/30"
|
||||
)}
|
||||
onClick={() => {
|
||||
onProjectSelect(project);
|
||||
@@ -871,22 +888,99 @@ function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
{/* Desktop delete button */}
|
||||
<button
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center touch:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteSession(project.name, session.id);
|
||||
}}
|
||||
title="Delete session (Delete)"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||
</button>
|
||||
{/* Desktop hover buttons */}
|
||||
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||||
{editingSession === session.id ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={editingSessionName}
|
||||
onChange={(e) => setEditingSessionName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
updateSessionSummary(project.name, session.id, editingSessionName);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingSession(null);
|
||||
setEditingSessionName('');
|
||||
}
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-32 px-2 py-1 text-xs border border-border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className="w-6 h-6 bg-green-50 hover:bg-green-100 dark:bg-green-900/20 dark:hover:bg-green-900/40 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateSessionSummary(project.name, session.id, editingSessionName);
|
||||
}}
|
||||
title="Save"
|
||||
>
|
||||
<Check className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
</button>
|
||||
<button
|
||||
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSession(null);
|
||||
setEditingSessionName('');
|
||||
}}
|
||||
title="Cancel"
|
||||
>
|
||||
<X className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Generate summary button */}
|
||||
{/* <button
|
||||
className="w-6 h-6 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:hover:bg-blue-900/40 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
generateSessionSummary(project.name, session.id);
|
||||
}}
|
||||
title="Generate AI summary for this session"
|
||||
disabled={generatingSummary[`${project.name}-${session.id}`]}
|
||||
>
|
||||
{generatingSummary[`${project.name}-${session.id}`] ? (
|
||||
<div className="w-3 h-3 animate-spin rounded-full border border-blue-600 dark:border-blue-400 border-t-transparent" />
|
||||
) : (
|
||||
<Sparkles className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
||||
)}
|
||||
</button> */}
|
||||
{/* Edit button */}
|
||||
<button
|
||||
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSession(session.id);
|
||||
setEditingSessionName(session.summary || 'New Session');
|
||||
}}
|
||||
title="Manually edit session name"
|
||||
>
|
||||
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
{/* Delete button */}
|
||||
<button
|
||||
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteSession(project.name, session.id);
|
||||
}}
|
||||
title="Delete this session permanently"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
|
||||
{/* Show More Sessions Button */}
|
||||
{getAllSessions(project).length > 0 && project.sessionMeta?.hasMore !== false && (
|
||||
<Button
|
||||
|
||||
24
src/components/TodoList.jsx
Normal file → Executable file
@@ -10,43 +10,43 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500 dark:text-green-400" />;
|
||||
case 'in_progress':
|
||||
return <Clock className="w-4 h-4 text-blue-500" />;
|
||||
return <Clock className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
|
||||
case 'pending':
|
||||
default:
|
||||
return <Circle className="w-4 h-4 text-gray-400" />;
|
||||
return <Circle className="w-4 h-4 text-gray-400 dark:text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'bg-green-100 text-green-800 border-green-200';
|
||||
return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800';
|
||||
case 'in_progress':
|
||||
return 'bg-blue-100 text-blue-800 border-blue-200';
|
||||
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800';
|
||||
case 'pending':
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600 border-gray-200';
|
||||
return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return 'bg-red-100 text-red-700 border-red-200';
|
||||
return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800';
|
||||
case 'medium':
|
||||
return 'bg-yellow-100 text-yellow-700 border-yellow-200';
|
||||
return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800';
|
||||
case 'low':
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600 border-gray-200';
|
||||
return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{isResult && (
|
||||
<div className="text-sm font-medium text-gray-700 mb-3">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
|
||||
</div>
|
||||
)}
|
||||
@@ -54,7 +54,7 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
{todos.map((todo) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
className="flex items-start gap-3 p-3 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md dark:shadow-gray-900/50 transition-shadow"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{getStatusIcon(todo.status)}
|
||||
@@ -62,7 +62,7 @@ const TodoList = ({ todos, isResult = false }) => {
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<p className={`text-sm font-medium ${todo.status === 'completed' ? 'line-through text-gray-500' : 'text-gray-900'}`}>
|
||||
<p className={`text-sm font-medium ${todo.status === 'completed' ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
|
||||
{todo.content}
|
||||
</p>
|
||||
|
||||
|
||||
46
src/components/ToolsSettings.jsx
Normal file → Executable file
@@ -3,9 +3,11 @@ import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, Plus, Settings, Shield, AlertTriangle } from 'lucide-react';
|
||||
import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
function ToolsSettings({ isOpen, onClose }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const [allowedTools, setAllowedTools] = useState([]);
|
||||
const [disallowedTools, setDisallowedTools] = useState([]);
|
||||
const [newAllowedTool, setNewAllowedTool] = useState('');
|
||||
@@ -140,6 +142,48 @@ function ToolsSettings({ isOpen, onClose }) {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4 md:p-6 space-y-6 md:space-y-8 pb-safe-area-inset-bottom">
|
||||
|
||||
{/* Theme Settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{isDarkMode ? <Moon className="w-5 h-5 text-blue-500" /> : <Sun className="w-5 h-5 text-yellow-500" />}
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
Appearance
|
||||
</h3>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">
|
||||
Dark Mode
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Toggle between light and dark themes
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={isDarkMode}
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<span className="sr-only">Toggle dark mode</span>
|
||||
<span
|
||||
className={`${
|
||||
isDarkMode ? 'translate-x-7' : 'translate-x-1'
|
||||
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Moon className="w-3.5 h-3.5 text-gray-700" />
|
||||
) : (
|
||||
<Sun className="w-3.5 h-3.5 text-yellow-500" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skip Permissions */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
0
src/components/ui/badge.jsx
Normal file → Executable file
0
src/components/ui/button.jsx
Normal file → Executable file
0
src/components/ui/input.jsx
Normal file → Executable file
0
src/components/ui/scroll-area.jsx
Normal file → Executable file
72
src/contexts/ThemeContext.jsx
Executable file
@@ -0,0 +1,72 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
// Check for saved theme preference or default to system preference
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
// Check localStorage first
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
return savedTheme === 'dark';
|
||||
}
|
||||
|
||||
// Check system preference
|
||||
if (window.matchMedia) {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Update document class and localStorage when theme changes
|
||||
useEffect(() => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
if (!window.matchMedia) return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = (e) => {
|
||||
// Only update if user hasn't manually set a preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (!savedTheme) {
|
||||
setIsDarkMode(e.matches);
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setIsDarkMode(prev => !prev);
|
||||
};
|
||||
|
||||
const value = {
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
109
src/hooks/useAudioRecorder.js
Executable file
@@ -0,0 +1,109 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
export function useAudioRecorder() {
|
||||
const [isRecording, setRecording] = useState(false);
|
||||
const [audioBlob, setAudioBlob] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const streamRef = useRef(null);
|
||||
const chunksRef = useRef([]);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
setAudioBlob(null);
|
||||
chunksRef.current = [];
|
||||
|
||||
// Request microphone access
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
sampleRate: 16000,
|
||||
}
|
||||
});
|
||||
|
||||
streamRef.current = stream;
|
||||
|
||||
// Determine supported MIME type
|
||||
const mimeType = MediaRecorder.isTypeSupported('audio/webm')
|
||||
? 'audio/webm'
|
||||
: 'audio/mp4';
|
||||
|
||||
// Create media recorder
|
||||
const recorder = new MediaRecorder(stream, { mimeType });
|
||||
mediaRecorderRef.current = recorder;
|
||||
|
||||
// Set up event handlers
|
||||
recorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
chunksRef.current.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = () => {
|
||||
// Create blob from chunks
|
||||
const blob = new Blob(chunksRef.current, { type: mimeType });
|
||||
setAudioBlob(blob);
|
||||
|
||||
// Clean up stream
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach(track => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onerror = (event) => {
|
||||
console.error('MediaRecorder error:', event);
|
||||
setError('Recording failed');
|
||||
setRecording(false);
|
||||
};
|
||||
|
||||
// Start recording
|
||||
recorder.start();
|
||||
setRecording(true);
|
||||
console.log('Recording started');
|
||||
} catch (err) {
|
||||
console.error('Failed to start recording:', err);
|
||||
setError(err.message || 'Failed to start recording');
|
||||
setRecording(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
console.log('Stop called, recorder state:', mediaRecorderRef.current?.state);
|
||||
|
||||
try {
|
||||
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
|
||||
mediaRecorderRef.current.stop();
|
||||
console.log('Recording stopped');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error stopping recorder:', err);
|
||||
}
|
||||
|
||||
// Always update state
|
||||
setRecording(false);
|
||||
|
||||
// Clean up stream if still active
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach(track => track.stop());
|
||||
streamRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setAudioBlob(null);
|
||||
setError(null);
|
||||
chunksRef.current = [];
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isRecording,
|
||||
audioBlob,
|
||||
error,
|
||||
start,
|
||||
stop,
|
||||
reset
|
||||
};
|
||||
}
|
||||
551
src/index.css
Normal file → Executable file
@@ -2,50 +2,69 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Global spinner animation - defined early to ensure it loads */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 217.2 91.2% 8%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 217.2 91.2% 8%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +72,7 @@
|
||||
* {
|
||||
@apply border-border;
|
||||
box-sizing: border-box;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -67,9 +87,106 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Global transition defaults */
|
||||
button,
|
||||
a,
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
[role="button"],
|
||||
.transition-all {
|
||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Color transitions for theme switching */
|
||||
* {
|
||||
transition: background-color 200ms ease-in-out,
|
||||
border-color 200ms ease-in-out,
|
||||
color 200ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Transform transitions */
|
||||
.transition-transform {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Opacity transitions */
|
||||
.transition-opacity {
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Scale transitions for interactions */
|
||||
.transition-scale {
|
||||
transition: transform 100ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Shadow transitions */
|
||||
.transition-shadow {
|
||||
transition: box-shadow 200ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Smooth hover transitions for interactive elements */
|
||||
button:hover,
|
||||
a:hover,
|
||||
[role="button"]:hover {
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
/* Active state transitions */
|
||||
button:active,
|
||||
a:active,
|
||||
[role="button"]:active {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
/* Focus transitions */
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
transition: outline-offset 150ms ease-out, box-shadow 150ms ease-out;
|
||||
}
|
||||
|
||||
/* Sidebar transitions */
|
||||
.sidebar-transition {
|
||||
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 300ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Modal and dropdown transitions */
|
||||
.modal-transition {
|
||||
transition: opacity 200ms ease-in-out,
|
||||
transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Chat message transitions */
|
||||
.message-transition {
|
||||
transition: opacity 300ms ease-in-out,
|
||||
transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Height transitions for expanding elements */
|
||||
.height-transition {
|
||||
transition: height 200ms ease-in-out,
|
||||
max-height 200ms ease-in-out;
|
||||
}
|
||||
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted-foreground)) transparent;
|
||||
@@ -77,6 +194,7 @@
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
@@ -91,6 +209,222 @@
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground) / 0.8);
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar styles */
|
||||
.dark .scrollbar-thin {
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.3);
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
/* Global scrollbar styles for main content areas */
|
||||
.dark::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dark::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
.dark::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(107, 114, 128, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(107, 114, 128, 0.7);
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styles */
|
||||
.dark {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(107, 114, 128, 0.5) rgba(31, 41, 55, 0.5);
|
||||
}
|
||||
|
||||
/* Ensure checkbox styling is preserved */
|
||||
input[type="checkbox"] {
|
||||
@apply accent-blue-600;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:focus {
|
||||
opacity: 1;
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Fix checkbox appearance in dark mode */
|
||||
.dark input[type="checkbox"] {
|
||||
background-color: rgb(31 41 55); /* gray-800 */
|
||||
border-color: rgb(75 85 99); /* gray-600 */
|
||||
color: rgb(37 99 235); /* blue-600 */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark input[type="checkbox"]:checked {
|
||||
background-color: rgb(37 99 235); /* blue-600 */
|
||||
border-color: rgb(37 99 235); /* blue-600 */
|
||||
}
|
||||
|
||||
.dark input[type="checkbox"]:focus {
|
||||
--tw-ring-color: rgb(59 130 246); /* blue-500 */
|
||||
--tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
|
||||
}
|
||||
|
||||
/* Fix radio button appearance in dark mode */
|
||||
.dark input[type="radio"] {
|
||||
background-color: rgb(31 41 55); /* gray-800 */
|
||||
border-color: rgb(75 85 99); /* gray-600 */
|
||||
color: rgb(37 99 235); /* blue-600 */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark input[type="radio"]:checked {
|
||||
background-color: rgb(37 99 235); /* blue-600 */
|
||||
border-color: rgb(37 99 235); /* blue-600 */
|
||||
}
|
||||
|
||||
.dark input[type="radio"]:focus {
|
||||
--tw-ring-color: rgb(59 130 246); /* blue-500 */
|
||||
--tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
|
||||
}
|
||||
|
||||
/* Ensure textarea text is always visible in dark mode */
|
||||
textarea {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
.dark textarea {
|
||||
color: rgb(243 244 246) !important; /* gray-100 */
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
caret-color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
/* Fix for focus state in dark mode */
|
||||
.dark textarea:focus {
|
||||
color: rgb(243 244 246) !important;
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
/* Fix for iOS/Safari dark mode textarea issues */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.dark textarea {
|
||||
background-color: transparent !important;
|
||||
color: rgb(243 244 246) !important;
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
.dark textarea:focus {
|
||||
background-color: transparent !important;
|
||||
color: rgb(243 244 246) !important;
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure parent container doesn't override textarea styles */
|
||||
.dark .bg-gray-800 textarea {
|
||||
color: rgb(243 244 246) !important;
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
/* Fix focus-within container issues in dark mode */
|
||||
.dark .focus-within\:ring-2:focus-within {
|
||||
background-color: rgb(31 41 55) !important; /* gray-800 */
|
||||
}
|
||||
|
||||
/* Ensure textarea remains transparent with visible text */
|
||||
.dark textarea.bg-transparent {
|
||||
background-color: transparent !important;
|
||||
color: rgb(243 244 246) !important;
|
||||
-webkit-text-fill-color: rgb(243 244 246) !important;
|
||||
}
|
||||
|
||||
/* Fix placeholder text color to be properly gray */
|
||||
textarea::placeholder {
|
||||
color: rgb(156 163 175) !important; /* gray-400 */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark textarea::placeholder {
|
||||
color: rgb(75 85 99) !important; /* gray-600 - darker gray */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* More specific selector for chat input textarea */
|
||||
.dark .bg-gray-800 textarea::placeholder,
|
||||
.dark textarea.bg-transparent::placeholder {
|
||||
color: rgb(75 85 99) !important; /* gray-600 - darker gray */
|
||||
opacity: 1 !important;
|
||||
-webkit-text-fill-color: rgb(75 85 99) !important;
|
||||
}
|
||||
|
||||
/* Custom class for chat input placeholder */
|
||||
.chat-input-placeholder::placeholder {
|
||||
color: rgb(156 163 175) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark .chat-input-placeholder::placeholder {
|
||||
color: rgb(75 85 99) !important;
|
||||
opacity: 1 !important;
|
||||
-webkit-text-fill-color: rgb(75 85 99) !important;
|
||||
}
|
||||
|
||||
.chat-input-placeholder::-webkit-input-placeholder {
|
||||
color: rgb(156 163 175) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark .chat-input-placeholder::-webkit-input-placeholder {
|
||||
color: rgb(75 85 99) !important;
|
||||
opacity: 1 !important;
|
||||
-webkit-text-fill-color: rgb(75 85 99) !important;
|
||||
}
|
||||
|
||||
/* WebKit specific placeholder styles */
|
||||
textarea::-webkit-input-placeholder {
|
||||
color: rgb(156 163 175) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark textarea::-webkit-input-placeholder {
|
||||
color: rgb(75 85 99) !important; /* gray-600 - darker gray */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Mozilla specific placeholder styles */
|
||||
textarea::-moz-placeholder {
|
||||
color: rgb(156 163 175) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark textarea::-moz-placeholder {
|
||||
color: rgb(75 85 99) !important; /* gray-600 - darker gray */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* IE/Edge specific placeholder styles */
|
||||
textarea:-ms-input-placeholder {
|
||||
color: rgb(156 163 175) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.dark textarea:-ms-input-placeholder {
|
||||
color: rgb(75 85 99) !important; /* gray-600 - darker gray */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile optimizations and components */
|
||||
@@ -102,6 +436,12 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Preserve checkbox visibility */
|
||||
input[type="checkbox"] {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[role="button"],
|
||||
.clickable,
|
||||
@@ -132,6 +472,12 @@
|
||||
@apply w-full sm:max-w-[95%];
|
||||
}
|
||||
|
||||
/* Session name truncation on mobile */
|
||||
.session-name-mobile {
|
||||
@apply truncate;
|
||||
max-width: calc(100vw - 120px); /* Account for sidebar padding and buttons */
|
||||
}
|
||||
|
||||
/* Enable text selection on mobile for terminal */
|
||||
.xterm,
|
||||
.xterm .xterm-viewport {
|
||||
@@ -182,6 +528,12 @@
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Preserve checkbox visibility on touch devices */
|
||||
input[type="checkbox"] {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Only disable hover states for interactive elements, not containers */
|
||||
button:hover,
|
||||
[role="button"]:hover,
|
||||
@@ -251,4 +603,137 @@
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide markdown backticks in prose content */
|
||||
.prose code::before,
|
||||
.prose code::after {
|
||||
content: "" !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Custom spinner animation for mobile compatibility */
|
||||
@layer utilities {
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Force hardware acceleration for smoother animation on mobile */
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Improved textarea styling */
|
||||
.chat-input-placeholder {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
|
||||
}
|
||||
|
||||
.chat-input-placeholder::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-input-placeholder::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.chat-input-placeholder::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.chat-input-placeholder::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
|
||||
.dark .chat-input-placeholder {
|
||||
scrollbar-color: rgba(107, 114, 128, 0.3) transparent;
|
||||
}
|
||||
|
||||
.dark .chat-input-placeholder::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
.dark .chat-input-placeholder::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(107, 114, 128, 0.5);
|
||||
}
|
||||
|
||||
/* Enhanced box shadow when textarea expands */
|
||||
.chat-input-expanded {
|
||||
box-shadow: 0 -5px 15px -3px rgba(0, 0, 0, 0.1), 0 -4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .chat-input-expanded {
|
||||
box-shadow: 0 -5px 15px -3px rgba(0, 0, 0, 0.3), 0 -4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Fix focus ring offset color in dark mode */
|
||||
.dark [class*="ring-offset"] {
|
||||
--tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
|
||||
}
|
||||
|
||||
/* Ensure buttons don't show white backgrounds in dark mode */
|
||||
.dark button:focus {
|
||||
--tw-ring-offset-color: rgb(31 41 55); /* gray-800 */
|
||||
}
|
||||
|
||||
/* Fix mobile select dropdown styling */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
select {
|
||||
font-size: 16px !important;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve select appearance in dark mode */
|
||||
.dark select {
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Fix select option text in mobile */
|
||||
select option {
|
||||
font-size: 16px !important;
|
||||
padding: 8px !important;
|
||||
background-color: var(--background) !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
.dark select option {
|
||||
background-color: rgb(31 41 55) !important;
|
||||
color: rgb(243 244 246) !important;
|
||||
}
|
||||
}
|
||||
0
src/lib/utils.js
Normal file → Executable file
0
src/main.jsx
Normal file → Executable file
0
src/utils/websocket.js
Normal file → Executable file
38
src/utils/whisper.js
Executable file
@@ -0,0 +1,38 @@
|
||||
export async function transcribeWithWhisper(audioBlob, onStatusChange) {
|
||||
const formData = new FormData();
|
||||
const fileName = `recording_${Date.now()}.webm`;
|
||||
const file = new File([audioBlob], fileName, { type: audioBlob.type });
|
||||
|
||||
formData.append('audio', file);
|
||||
|
||||
const whisperMode = window.localStorage.getItem('whisperMode') || 'default';
|
||||
formData.append('mode', whisperMode);
|
||||
|
||||
try {
|
||||
// Start with transcribing state
|
||||
if (onStatusChange) {
|
||||
onStatusChange('transcribing');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.error ||
|
||||
`Transcription error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.text || '';
|
||||
} catch (error) {
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
throw new Error('Cannot connect to server. Please ensure the backend is running.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
0
tailwind.config.js
Normal file → Executable file
32
vite.config.js
Normal file → Executable file
@@ -1,19 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: process.env.VITE_PORT || 3001,
|
||||
proxy: {
|
||||
'/api': `http://localhost:${process.env.PORT || 3002}`,
|
||||
'/ws': {
|
||||
target: `ws://localhost:${process.env.PORT || 3002}`,
|
||||
ws: true
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
// Load env file based on `mode` in the current working directory.
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: parseInt(env.VITE_PORT) || 3001,
|
||||
proxy: {
|
||||
'/api': `http://localhost:${env.PORT || 3002}`,
|
||||
'/ws': {
|
||||
target: `ws://localhost:${env.PORT || 3002}`,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist'
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist'
|
||||
}
|
||||
})
|
||||