opensentinel 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/bot-KJ26BG56.js +15 -0
- package/dist/bot-KJ26BG56.js.map +1 -0
- package/dist/charts-MMXM6BWW.js +241 -0
- package/dist/charts-MMXM6BWW.js.map +1 -0
- package/dist/chunk-4LVWXUNC.js +1079 -0
- package/dist/chunk-4LVWXUNC.js.map +1 -0
- package/dist/chunk-4TG2IG5K.js +5249 -0
- package/dist/chunk-4TG2IG5K.js.map +1 -0
- package/dist/chunk-6DRDKB45.js +251 -0
- package/dist/chunk-6DRDKB45.js.map +1 -0
- package/dist/chunk-6SNHU3CY.js +123 -0
- package/dist/chunk-6SNHU3CY.js.map +1 -0
- package/dist/chunk-CI6Q63MM.js +1613 -0
- package/dist/chunk-CI6Q63MM.js.map +1 -0
- package/dist/chunk-CQ4JURG7.js +57 -0
- package/dist/chunk-CQ4JURG7.js.map +1 -0
- package/dist/chunk-F6QUZQGI.js +51 -0
- package/dist/chunk-F6QUZQGI.js.map +1 -0
- package/dist/chunk-GK3E2I7A.js +216 -0
- package/dist/chunk-GK3E2I7A.js.map +1 -0
- package/dist/chunk-GUBEEYDW.js +211 -0
- package/dist/chunk-GUBEEYDW.js.map +1 -0
- package/dist/chunk-GVJVEWHI.js +29 -0
- package/dist/chunk-GVJVEWHI.js.map +1 -0
- package/dist/chunk-HH2HBTQM.js +806 -0
- package/dist/chunk-HH2HBTQM.js.map +1 -0
- package/dist/chunk-JXUP2X7V.js +129 -0
- package/dist/chunk-JXUP2X7V.js.map +1 -0
- package/dist/chunk-KHNYJY2Z.js +178 -0
- package/dist/chunk-KHNYJY2Z.js.map +1 -0
- package/dist/chunk-L3F43VPB.js +652 -0
- package/dist/chunk-L3F43VPB.js.map +1 -0
- package/dist/chunk-L3PDU3XN.js +803 -0
- package/dist/chunk-L3PDU3XN.js.map +1 -0
- package/dist/chunk-NSBPE2FW.js +17 -0
- package/dist/chunk-NSBPE2FW.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.js +374 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +57 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +8 -0
- package/dist/commands/stop.js +37 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/utils.d.ts +50 -0
- package/dist/commands/utils.js +36 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/discord-ZOJFTVTB.js +49 -0
- package/dist/discord-ZOJFTVTB.js.map +1 -0
- package/dist/imessage-JFRB6EJ7.js +14 -0
- package/dist/imessage-JFRB6EJ7.js.map +1 -0
- package/dist/lib.d.ts +855 -0
- package/dist/lib.js +274 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-LS7Q3Z5W.js +30 -0
- package/dist/mcp-LS7Q3Z5W.js.map +1 -0
- package/dist/scheduler-EZ7CZMCS.js +42 -0
- package/dist/scheduler-EZ7CZMCS.js.map +1 -0
- package/dist/signal-T3MCSULM.js +14 -0
- package/dist/signal-T3MCSULM.js.map +1 -0
- package/dist/slack-N2M4FHAJ.js +54 -0
- package/dist/slack-N2M4FHAJ.js.map +1 -0
- package/dist/src-K7GASHRH.js +430 -0
- package/dist/src-K7GASHRH.js.map +1 -0
- package/dist/tools-24GZHYRF.js +16 -0
- package/dist/tools-24GZHYRF.js.map +1 -0
- package/dist/whatsapp-VCRUPAO5.js +14 -0
- package/dist/whatsapp-VCRUPAO5.js.map +1 -0
- package/drizzle/0000_chilly_shinobi_shaw.sql +75 -0
- package/drizzle/0001_freezing_shape.sql +274 -0
- package/drizzle/meta/0000_snapshot.json +529 -0
- package/drizzle/meta/0001_snapshot.json +2576 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenSentinel Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# OpenSentinel
|
|
2
|
+
|
|
3
|
+
A self-hosted personal AI assistant powered by Claude, with JARVIS-like capabilities.
|
|
4
|
+
|
|
5
|
+
**Live**: [app.opensentinel.ai](https://app.opensentinel.ai) | **Website**: [opensentinel.ai](https://opensentinel.ai)
|
|
6
|
+
|
|
7
|
+
## What is OpenSentinel?
|
|
8
|
+
|
|
9
|
+
OpenSentinel is your own personal AI assistant that runs on your infrastructure. Think of it like having Jarvis from Iron Man - you can talk to it, ask questions, and it can do things for you.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Core Capabilities
|
|
14
|
+
- Answer questions and have conversations
|
|
15
|
+
- Run commands on your computer (sandboxed)
|
|
16
|
+
- Read and write files
|
|
17
|
+
- Browse the web with Playwright
|
|
18
|
+
- Search the internet
|
|
19
|
+
- Set reminders and scheduled tasks
|
|
20
|
+
- Remember things about you (RAG memory)
|
|
21
|
+
- Respond with voice (JARVIS voice via ElevenLabs)
|
|
22
|
+
|
|
23
|
+
### Advanced Voice
|
|
24
|
+
- Wake word detection ("Hey OpenSentinel")
|
|
25
|
+
- Continuous conversation mode
|
|
26
|
+
- Voice activity detection (VAD)
|
|
27
|
+
- Speaker diarization (multi-person)
|
|
28
|
+
- Noise cancellation
|
|
29
|
+
- Voice note summarization
|
|
30
|
+
|
|
31
|
+
### Communication Platforms
|
|
32
|
+
- Telegram bot with voice support
|
|
33
|
+
- Discord bot with slash commands and voice channels
|
|
34
|
+
- Slack bot with app mentions and threads
|
|
35
|
+
- Web dashboard
|
|
36
|
+
- REST API
|
|
37
|
+
|
|
38
|
+
### Device Triggers
|
|
39
|
+
- iOS/macOS Shortcuts integration
|
|
40
|
+
- Bluetooth proximity activation
|
|
41
|
+
- Geofencing (location-based)
|
|
42
|
+
- NFC tag scanning
|
|
43
|
+
- Calendar triggers (Google, Outlook, iCal)
|
|
44
|
+
|
|
45
|
+
### Multi-Modal Input
|
|
46
|
+
- Image understanding and analysis
|
|
47
|
+
- Document OCR
|
|
48
|
+
- Screenshot interpretation
|
|
49
|
+
- Video summarization
|
|
50
|
+
- Audio transcription
|
|
51
|
+
|
|
52
|
+
### Sub-Agent System
|
|
53
|
+
- Research agent (web search, synthesis)
|
|
54
|
+
- Coding agent (implementation, debugging)
|
|
55
|
+
- Writing agent (drafts, editing)
|
|
56
|
+
- Analysis agent (data processing)
|
|
57
|
+
- Agent collaboration and task coordination
|
|
58
|
+
|
|
59
|
+
### File Generation
|
|
60
|
+
- PDF documents
|
|
61
|
+
- Word documents (.docx)
|
|
62
|
+
- PowerPoint presentations (.pptx)
|
|
63
|
+
- Excel spreadsheets
|
|
64
|
+
- Charts and diagrams
|
|
65
|
+
- AI image generation (DALL-E)
|
|
66
|
+
|
|
67
|
+
### Personality System
|
|
68
|
+
- 15 domain expert modes (coding, legal, medical, finance, etc.)
|
|
69
|
+
- Mood detection and adaptation
|
|
70
|
+
- Configurable personas (formal, casual, snarky)
|
|
71
|
+
- Verbosity and humor controls
|
|
72
|
+
|
|
73
|
+
### Security
|
|
74
|
+
- 2FA for sensitive operations
|
|
75
|
+
- Biometric verification
|
|
76
|
+
- Memory vault (encrypted storage)
|
|
77
|
+
- Audit logging
|
|
78
|
+
- GDPR compliance tools
|
|
79
|
+
- Rate limiting
|
|
80
|
+
|
|
81
|
+
### Enterprise Features
|
|
82
|
+
- Multi-user support
|
|
83
|
+
- Team knowledge base
|
|
84
|
+
- Usage quotas
|
|
85
|
+
- SSO integration (SAML, OAuth, OIDC)
|
|
86
|
+
- Kubernetes deployment
|
|
87
|
+
|
|
88
|
+
### Observability
|
|
89
|
+
- Metrics dashboard
|
|
90
|
+
- Replay mode (re-run conversations)
|
|
91
|
+
- Tool dry-run (preview without executing)
|
|
92
|
+
- Prompt inspector
|
|
93
|
+
- Alerting (anomaly, cost, errors)
|
|
94
|
+
|
|
95
|
+
### Integrations
|
|
96
|
+
- **Email**: IMAP/SMTP with AI inbox summarization
|
|
97
|
+
- **SMS/Phone**: Twilio for calls and texts
|
|
98
|
+
- **GitHub**: Repos, issues, PRs, AI code review
|
|
99
|
+
- **Notion**: Pages, databases, search, sync
|
|
100
|
+
- **Home Assistant**: Smart home device control
|
|
101
|
+
- **Spotify**: Playback, playlists, search
|
|
102
|
+
- **Cloud Storage**: Google Drive, Dropbox
|
|
103
|
+
- **Finance**: Crypto, stocks, currency, portfolio tracking
|
|
104
|
+
|
|
105
|
+
### Vision & Documents
|
|
106
|
+
- Screen capture and webcam analysis
|
|
107
|
+
- Document ingestion (PDF, DOCX, TXT, etc.)
|
|
108
|
+
- Knowledge base with vector search
|
|
109
|
+
- Enhanced OCR with layout detection
|
|
110
|
+
|
|
111
|
+
### Workflow Automation
|
|
112
|
+
- IFTTT-like trigger → action workflows
|
|
113
|
+
- Time, webhook, and event triggers
|
|
114
|
+
- Built-in workflow templates
|
|
115
|
+
|
|
116
|
+
### Desktop & Browser Apps
|
|
117
|
+
- **Electron Desktop App**: System tray, global hotkeys (Ctrl+Shift+O)
|
|
118
|
+
- **Browser Extension**: Chrome/Firefox popup chat, context menu
|
|
119
|
+
|
|
120
|
+
## Quick Start
|
|
121
|
+
|
|
122
|
+
### Prerequisites
|
|
123
|
+
- [Bun](https://bun.sh) runtime
|
|
124
|
+
- [Docker](https://docker.com) for PostgreSQL and Redis
|
|
125
|
+
- Telegram account
|
|
126
|
+
- API keys (Claude, OpenAI, ElevenLabs)
|
|
127
|
+
|
|
128
|
+
### Installation
|
|
129
|
+
|
|
130
|
+
1. **Clone and install dependencies**
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/dsiemon2/OpenSentinel.git
|
|
133
|
+
cd OpenSentinel
|
|
134
|
+
bun install
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
2. **Set up environment variables**
|
|
138
|
+
```bash
|
|
139
|
+
cp .env.example .env
|
|
140
|
+
# Edit .env with your API keys
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
3. **Start database services**
|
|
144
|
+
```bash
|
|
145
|
+
docker compose up -d
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
4. **Run database migrations**
|
|
149
|
+
```bash
|
|
150
|
+
bun run db:migrate
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
5. **Build the web dashboard**
|
|
154
|
+
```bash
|
|
155
|
+
cd src/web && bun install && bun run build && cd ../..
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
6. **Start OpenSentinel**
|
|
159
|
+
```bash
|
|
160
|
+
bun run start
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## How to Use
|
|
164
|
+
|
|
165
|
+
### Telegram (Primary)
|
|
166
|
+
1. Open Telegram
|
|
167
|
+
2. Search for your bot (e.g., `@JarvisElectronBot`)
|
|
168
|
+
3. Send a message like "Hello!"
|
|
169
|
+
|
|
170
|
+
### Web Dashboard
|
|
171
|
+
1. Open http://localhost:8030 in your browser
|
|
172
|
+
2. Type a message and click Send
|
|
173
|
+
|
|
174
|
+
### API
|
|
175
|
+
```bash
|
|
176
|
+
curl -X POST http://localhost:8030/api/ask \
|
|
177
|
+
-H "Content-Type: application/json" \
|
|
178
|
+
-d '{"message": "Hello, what can you do?"}'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Commands (Telegram)
|
|
182
|
+
|
|
183
|
+
| Command | Description |
|
|
184
|
+
|---------|-------------|
|
|
185
|
+
| `/start` | Welcome message |
|
|
186
|
+
| `/help` | Show help |
|
|
187
|
+
| `/clear` | Clear conversation history |
|
|
188
|
+
| `/remind 5m Take a break` | Set a reminder |
|
|
189
|
+
| `/mode productivity` | Switch to productivity mode |
|
|
190
|
+
| `/expert coding` | Activate coding expert |
|
|
191
|
+
|
|
192
|
+
## Architecture
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
196
|
+
│ OPENSENTINEL v2.0 │
|
|
197
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
198
|
+
│ Inputs │ Core │ Outputs │
|
|
199
|
+
│ ────── │ ──── │ ─────── │
|
|
200
|
+
│ • Telegram │ • Claude Brain │ • Text │
|
|
201
|
+
│ • Web Dashboard │ • Memory/RAG │ • Voice TTS │
|
|
202
|
+
│ • REST API │ • Tool Router │ • Files (PDF, │
|
|
203
|
+
│ • Voice (Wake Word) │ • Scheduler │ Word, Excel, │
|
|
204
|
+
│ • Device Triggers │ • Sub-Agents │ PPT, Images) │
|
|
205
|
+
│ • Calendar │ • Plugins │ │
|
|
206
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
207
|
+
│ Tools: Shell, Files, Browser, Search, OCR, Screenshots, │
|
|
208
|
+
│ Video, Image Analysis, File Generation │
|
|
209
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
210
|
+
│ Intelligence: Predictive, Relationship Graph, Temporal, │
|
|
211
|
+
│ Multi-lingual, Domain Experts │
|
|
212
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
213
|
+
│ Security: 2FA, Biometric, Vault, Audit, GDPR, Rate Limiting │
|
|
214
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
215
|
+
│ Enterprise: Multi-User, Team Memory, Quotas, SSO, Kubernetes │
|
|
216
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
217
|
+
│ Data: PostgreSQL + pgvector │ Redis │
|
|
218
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Project Structure
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
src/
|
|
225
|
+
├── index.ts # Entry point
|
|
226
|
+
├── config/env.ts # Environment config
|
|
227
|
+
├── core/
|
|
228
|
+
│ ├── brain.ts # Claude API + tool execution
|
|
229
|
+
│ ├── memory.ts # RAG memory system
|
|
230
|
+
│ ├── scheduler.ts # BullMQ task scheduler
|
|
231
|
+
│ ├── agents/ # Sub-agent system
|
|
232
|
+
│ ├── enterprise/ # Multi-user, SSO, quotas
|
|
233
|
+
│ ├── intelligence/ # Predictive, relationship, temporal
|
|
234
|
+
│ ├── molt/ # Evolution, achievements, modes
|
|
235
|
+
│ ├── observability/ # Metrics, replay, alerting
|
|
236
|
+
│ ├── personality/ # Personas, mood, domain experts
|
|
237
|
+
│ ├── plugins/ # Plugin system
|
|
238
|
+
│ ├── security/ # 2FA, vault, GDPR, audit
|
|
239
|
+
│ └── workflows/ # Automation engine
|
|
240
|
+
├── inputs/
|
|
241
|
+
│ ├── telegram/ # Telegram bot
|
|
242
|
+
│ ├── discord/ # Discord bot
|
|
243
|
+
│ ├── slack/ # Slack bot
|
|
244
|
+
│ ├── api/ # REST API
|
|
245
|
+
│ ├── calendar/ # Google, Outlook, iCal
|
|
246
|
+
│ ├── triggers/ # Shortcuts, Bluetooth, NFC, Geofence
|
|
247
|
+
│ └── voice/ # Wake word, VAD, diarization
|
|
248
|
+
├── integrations/
|
|
249
|
+
│ ├── email/ # IMAP/SMTP email
|
|
250
|
+
│ ├── twilio/ # SMS/Phone calls
|
|
251
|
+
│ ├── github/ # GitHub API
|
|
252
|
+
│ ├── notion/ # Notion API
|
|
253
|
+
│ ├── homeassistant/ # Home Assistant
|
|
254
|
+
│ ├── spotify/ # Spotify API
|
|
255
|
+
│ ├── cloud-storage/ # Google Drive, Dropbox
|
|
256
|
+
│ ├── finance/ # Crypto, stocks, currency
|
|
257
|
+
│ ├── documents/ # Document ingestion
|
|
258
|
+
│ └── vision/ # Screen/webcam capture
|
|
259
|
+
├── tools/
|
|
260
|
+
│ ├── file-generation/ # PDF, Word, Excel, PPT, images
|
|
261
|
+
│ └── rendering/ # Math, code, markdown
|
|
262
|
+
├── outputs/
|
|
263
|
+
│ ├── stt.ts # Speech-to-text
|
|
264
|
+
│ └── tts.ts # Text-to-speech
|
|
265
|
+
├── db/
|
|
266
|
+
│ └── schema.ts # Drizzle ORM schema
|
|
267
|
+
└── web/ # React dashboard
|
|
268
|
+
|
|
269
|
+
desktop/ # Electron desktop app
|
|
270
|
+
extension/ # Browser extension
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Ports
|
|
274
|
+
|
|
275
|
+
| Service | Port |
|
|
276
|
+
|---------|------|
|
|
277
|
+
| OpenSentinel API + Dashboard | 8030 |
|
|
278
|
+
| PostgreSQL | 5445 |
|
|
279
|
+
| Redis | 6379 |
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBot
|
|
3
|
+
} from "./chunk-6DRDKB45.js";
|
|
4
|
+
import "./chunk-F6QUZQGI.js";
|
|
5
|
+
import "./chunk-GVJVEWHI.js";
|
|
6
|
+
import "./chunk-4LVWXUNC.js";
|
|
7
|
+
import "./chunk-CI6Q63MM.js";
|
|
8
|
+
import "./chunk-4TG2IG5K.js";
|
|
9
|
+
import "./chunk-CQ4JURG7.js";
|
|
10
|
+
import "./chunk-L3F43VPB.js";
|
|
11
|
+
import "./chunk-NSBPE2FW.js";
|
|
12
|
+
export {
|
|
13
|
+
createBot
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=bot-KJ26BG56.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPathAllowed
|
|
3
|
+
} from "./chunk-CQ4JURG7.js";
|
|
4
|
+
import "./chunk-NSBPE2FW.js";
|
|
5
|
+
|
|
6
|
+
// src/tools/file-generation/charts.ts
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { tmpdir } from "os";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
10
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
11
|
+
import { dirname } from "path";
|
|
12
|
+
var DEFAULT_COLORS = [
|
|
13
|
+
"#4285f4",
|
|
14
|
+
// Blue
|
|
15
|
+
"#ea4335",
|
|
16
|
+
// Red
|
|
17
|
+
"#fbbc04",
|
|
18
|
+
// Yellow
|
|
19
|
+
"#34a853",
|
|
20
|
+
// Green
|
|
21
|
+
"#ff6d01",
|
|
22
|
+
// Orange
|
|
23
|
+
"#46bdc6",
|
|
24
|
+
// Teal
|
|
25
|
+
"#7baaf7",
|
|
26
|
+
// Light Blue
|
|
27
|
+
"#f07b72"
|
|
28
|
+
// Light Red
|
|
29
|
+
];
|
|
30
|
+
function getTempPath() {
|
|
31
|
+
const id = randomBytes(8).toString("hex");
|
|
32
|
+
return join(tmpdir(), `sentinel-chart-${id}.svg`);
|
|
33
|
+
}
|
|
34
|
+
function generateBarChartSVG(data, options) {
|
|
35
|
+
const width = options.width || 600;
|
|
36
|
+
const height = options.height || 400;
|
|
37
|
+
const padding = 60;
|
|
38
|
+
const chartWidth = width - padding * 2;
|
|
39
|
+
const chartHeight = height - padding * 2;
|
|
40
|
+
const allValues = data.datasets.flatMap((d) => d.data);
|
|
41
|
+
const maxValue = Math.max(...allValues) * 1.1;
|
|
42
|
+
const barGroupWidth = chartWidth / data.labels.length;
|
|
43
|
+
const barWidth = barGroupWidth / (data.datasets.length + 1);
|
|
44
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
|
|
45
|
+
svg += `<rect width="100%" height="100%" fill="white"/>`;
|
|
46
|
+
if (options.title) {
|
|
47
|
+
svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(options.title)}</text>`;
|
|
48
|
+
}
|
|
49
|
+
svg += `<line x1="${padding}" y1="${padding}" x2="${padding}" y2="${height - padding}" stroke="#ccc"/>`;
|
|
50
|
+
svg += `<line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="#ccc"/>`;
|
|
51
|
+
for (let i = 0; i <= 5; i++) {
|
|
52
|
+
const y = padding + chartHeight * i / 5;
|
|
53
|
+
const value = Math.round(maxValue * (1 - i / 5));
|
|
54
|
+
svg += `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" stroke="#eee"/>`;
|
|
55
|
+
svg += `<text x="${padding - 10}" y="${y + 4}" text-anchor="end" font-size="10">${value}</text>`;
|
|
56
|
+
}
|
|
57
|
+
data.datasets.forEach((dataset, datasetIndex) => {
|
|
58
|
+
const color = dataset.backgroundColor || DEFAULT_COLORS[datasetIndex % DEFAULT_COLORS.length];
|
|
59
|
+
dataset.data.forEach((value, index) => {
|
|
60
|
+
const x = padding + index * barGroupWidth + datasetIndex * barWidth + barWidth / 2;
|
|
61
|
+
const barHeight = value / maxValue * chartHeight;
|
|
62
|
+
const y = height - padding - barHeight;
|
|
63
|
+
svg += `<rect x="${x}" y="${y}" width="${barWidth * 0.8}" height="${barHeight}" fill="${color}"/>`;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
data.labels.forEach((label, index) => {
|
|
67
|
+
const x = padding + index * barGroupWidth + barGroupWidth / 2;
|
|
68
|
+
svg += `<text x="${x}" y="${height - padding + 20}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>`;
|
|
69
|
+
});
|
|
70
|
+
if (options.legend !== false && data.datasets.length > 1) {
|
|
71
|
+
data.datasets.forEach((dataset, index) => {
|
|
72
|
+
const legendX = width - padding - 100;
|
|
73
|
+
const legendY = padding + index * 20;
|
|
74
|
+
const color = dataset.backgroundColor || DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
|
75
|
+
svg += `<rect x="${legendX}" y="${legendY}" width="12" height="12" fill="${color}"/>`;
|
|
76
|
+
svg += `<text x="${legendX + 16}" y="${legendY + 10}" font-size="10">${escapeXml(dataset.label)}</text>`;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
svg += "</svg>";
|
|
80
|
+
return svg;
|
|
81
|
+
}
|
|
82
|
+
function generateLineChartSVG(data, options) {
|
|
83
|
+
const width = options.width || 600;
|
|
84
|
+
const height = options.height || 400;
|
|
85
|
+
const padding = 60;
|
|
86
|
+
const chartWidth = width - padding * 2;
|
|
87
|
+
const chartHeight = height - padding * 2;
|
|
88
|
+
const allValues = data.datasets.flatMap((d) => d.data);
|
|
89
|
+
const maxValue = Math.max(...allValues) * 1.1;
|
|
90
|
+
const pointSpacing = chartWidth / (data.labels.length - 1 || 1);
|
|
91
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
|
|
92
|
+
svg += `<rect width="100%" height="100%" fill="white"/>`;
|
|
93
|
+
if (options.title) {
|
|
94
|
+
svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(options.title)}</text>`;
|
|
95
|
+
}
|
|
96
|
+
svg += `<line x1="${padding}" y1="${padding}" x2="${padding}" y2="${height - padding}" stroke="#ccc"/>`;
|
|
97
|
+
svg += `<line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="#ccc"/>`;
|
|
98
|
+
for (let i = 0; i <= 5; i++) {
|
|
99
|
+
const y = padding + chartHeight * i / 5;
|
|
100
|
+
const value = Math.round(maxValue * (1 - i / 5));
|
|
101
|
+
svg += `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" stroke="#eee"/>`;
|
|
102
|
+
svg += `<text x="${padding - 10}" y="${y + 4}" text-anchor="end" font-size="10">${value}</text>`;
|
|
103
|
+
}
|
|
104
|
+
data.datasets.forEach((dataset, datasetIndex) => {
|
|
105
|
+
const color = dataset.borderColor || DEFAULT_COLORS[datasetIndex % DEFAULT_COLORS.length];
|
|
106
|
+
const points = [];
|
|
107
|
+
dataset.data.forEach((value, index) => {
|
|
108
|
+
const x = padding + index * pointSpacing;
|
|
109
|
+
const y = height - padding - value / maxValue * chartHeight;
|
|
110
|
+
points.push(`${x},${y}`);
|
|
111
|
+
});
|
|
112
|
+
svg += `<polyline points="${points.join(" ")}" fill="none" stroke="${color}" stroke-width="2"/>`;
|
|
113
|
+
dataset.data.forEach((value, index) => {
|
|
114
|
+
const x = padding + index * pointSpacing;
|
|
115
|
+
const y = height - padding - value / maxValue * chartHeight;
|
|
116
|
+
svg += `<circle cx="${x}" cy="${y}" r="4" fill="${color}"/>`;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
data.labels.forEach((label, index) => {
|
|
120
|
+
const x = padding + index * pointSpacing;
|
|
121
|
+
svg += `<text x="${x}" y="${height - padding + 20}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>`;
|
|
122
|
+
});
|
|
123
|
+
if (options.legend !== false && data.datasets.length > 1) {
|
|
124
|
+
data.datasets.forEach((dataset, index) => {
|
|
125
|
+
const legendX = width - padding - 100;
|
|
126
|
+
const legendY = padding + index * 20;
|
|
127
|
+
const color = dataset.borderColor || DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
|
128
|
+
svg += `<line x1="${legendX}" y1="${legendY + 6}" x2="${legendX + 12}" y2="${legendY + 6}" stroke="${color}" stroke-width="2"/>`;
|
|
129
|
+
svg += `<text x="${legendX + 16}" y="${legendY + 10}" font-size="10">${escapeXml(dataset.label)}</text>`;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
svg += "</svg>";
|
|
133
|
+
return svg;
|
|
134
|
+
}
|
|
135
|
+
function generatePieChartSVG(data, options) {
|
|
136
|
+
const width = options.width || 400;
|
|
137
|
+
const height = options.height || 400;
|
|
138
|
+
const centerX = width / 2;
|
|
139
|
+
const centerY = height / 2;
|
|
140
|
+
const radius = Math.min(width, height) / 2 - 60;
|
|
141
|
+
const values = data.datasets[0]?.data || [];
|
|
142
|
+
const total = values.reduce((sum, v) => sum + v, 0);
|
|
143
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
|
|
144
|
+
svg += `<rect width="100%" height="100%" fill="white"/>`;
|
|
145
|
+
if (options.title) {
|
|
146
|
+
svg += `<text x="${width / 2}" y="25" text-anchor="middle" font-size="16" font-weight="bold">${escapeXml(options.title)}</text>`;
|
|
147
|
+
}
|
|
148
|
+
let startAngle = -Math.PI / 2;
|
|
149
|
+
values.forEach((value, index) => {
|
|
150
|
+
const sliceAngle = value / total * 2 * Math.PI;
|
|
151
|
+
const endAngle = startAngle + sliceAngle;
|
|
152
|
+
const x1 = centerX + radius * Math.cos(startAngle);
|
|
153
|
+
const y1 = centerY + radius * Math.sin(startAngle);
|
|
154
|
+
const x2 = centerX + radius * Math.cos(endAngle);
|
|
155
|
+
const y2 = centerY + radius * Math.sin(endAngle);
|
|
156
|
+
const largeArcFlag = sliceAngle > Math.PI ? 1 : 0;
|
|
157
|
+
const colors = data.datasets[0]?.backgroundColor;
|
|
158
|
+
const color = Array.isArray(colors) ? colors[index % colors.length] : DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
|
159
|
+
svg += `<path d="M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z" fill="${color}"/>`;
|
|
160
|
+
const labelAngle = startAngle + sliceAngle / 2;
|
|
161
|
+
const labelX = centerX + radius * 0.7 * Math.cos(labelAngle);
|
|
162
|
+
const labelY = centerY + radius * 0.7 * Math.sin(labelAngle);
|
|
163
|
+
const percentage = Math.round(value / total * 100);
|
|
164
|
+
svg += `<text x="${labelX}" y="${labelY}" text-anchor="middle" font-size="12" fill="white" font-weight="bold">${percentage}%</text>`;
|
|
165
|
+
startAngle = endAngle;
|
|
166
|
+
});
|
|
167
|
+
if (options.legend !== false) {
|
|
168
|
+
data.labels.forEach((label, index) => {
|
|
169
|
+
const legendY = height - 40 + index % 2 * 20;
|
|
170
|
+
const legendX = 20 + Math.floor(index / 2) * 120;
|
|
171
|
+
const colors = data.datasets[0]?.backgroundColor;
|
|
172
|
+
const color = Array.isArray(colors) ? colors[index % colors.length] : DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
|
173
|
+
svg += `<rect x="${legendX}" y="${legendY}" width="12" height="12" fill="${color}"/>`;
|
|
174
|
+
svg += `<text x="${legendX + 16}" y="${legendY + 10}" font-size="10">${escapeXml(label)}</text>`;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
svg += "</svg>";
|
|
178
|
+
return svg;
|
|
179
|
+
}
|
|
180
|
+
function escapeXml(str) {
|
|
181
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
182
|
+
}
|
|
183
|
+
async function generateChart(type, data, filename, options = {}) {
|
|
184
|
+
const filePath = filename ? isPathAllowed(filename) ? filename : join(tmpdir(), filename) : getTempPath();
|
|
185
|
+
try {
|
|
186
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
187
|
+
let svgContent;
|
|
188
|
+
switch (type) {
|
|
189
|
+
case "bar":
|
|
190
|
+
svgContent = generateBarChartSVG(data, options);
|
|
191
|
+
break;
|
|
192
|
+
case "line":
|
|
193
|
+
case "area":
|
|
194
|
+
svgContent = generateLineChartSVG(data, options);
|
|
195
|
+
break;
|
|
196
|
+
case "pie":
|
|
197
|
+
case "doughnut":
|
|
198
|
+
svgContent = generatePieChartSVG(data, options);
|
|
199
|
+
break;
|
|
200
|
+
case "scatter":
|
|
201
|
+
svgContent = generateLineChartSVG(data, { ...options });
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
return { success: false, error: `Unsupported chart type: ${type}` };
|
|
205
|
+
}
|
|
206
|
+
await writeFile(filePath, svgContent, "utf-8");
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
filePath,
|
|
210
|
+
svgContent
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
error: error instanceof Error ? error.message : String(error)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function quickChart(type, labels, values, title, filename) {
|
|
220
|
+
const data = {
|
|
221
|
+
labels,
|
|
222
|
+
datasets: [
|
|
223
|
+
{
|
|
224
|
+
label: title || "Data",
|
|
225
|
+
data: values,
|
|
226
|
+
backgroundColor: DEFAULT_COLORS
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
return generateChart(type, data, filename, { title });
|
|
231
|
+
}
|
|
232
|
+
var charts_default = {
|
|
233
|
+
generateChart,
|
|
234
|
+
quickChart
|
|
235
|
+
};
|
|
236
|
+
export {
|
|
237
|
+
charts_default as default,
|
|
238
|
+
generateChart,
|
|
239
|
+
quickChart
|
|
240
|
+
};
|
|
241
|
+
//# sourceMappingURL=charts-MMXM6BWW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/file-generation/charts.ts"],"sourcesContent":["import { join } from \"path\";\nimport { tmpdir } from \"os\";\nimport { randomBytes } from \"crypto\";\nimport { writeFile, mkdir } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { isPathAllowed } from \"../../utils/paths\";\n\nexport type ChartType = \"bar\" | \"line\" | \"pie\" | \"doughnut\" | \"scatter\" | \"area\";\n\nexport interface ChartDataset {\n label: string;\n data: number[];\n backgroundColor?: string | string[];\n borderColor?: string;\n}\n\nexport interface ChartData {\n labels: string[];\n datasets: ChartDataset[];\n}\n\nexport interface ChartOptions {\n title?: string;\n width?: number;\n height?: number;\n legend?: boolean;\n xAxisLabel?: string;\n yAxisLabel?: string;\n}\n\nexport interface ChartResult {\n success: boolean;\n filePath?: string;\n svgContent?: string;\n error?: string;\n}\n\n// Default colors for charts\nconst DEFAULT_COLORS = [\n \"#4285f4\", // Blue\n \"#ea4335\", // Red\n \"#fbbc04\", // Yellow\n \"#34a853\", // Green\n \"#ff6d01\", // Orange\n \"#46bdc6\", // Teal\n \"#7baaf7\", // Light Blue\n \"#f07b72\", // Light Red\n];\n\n// Generate temp file path\nfunction getTempPath(): string {\n const id = randomBytes(8).toString(\"hex\");\n return join(tmpdir(), `sentinel-chart-${id}.svg`);\n}\n\n// Generate SVG bar chart\nfunction generateBarChartSVG(data: ChartData, options: ChartOptions): string {\n const width = options.width || 600;\n const height = options.height || 400;\n const padding = 60;\n const chartWidth = width - padding * 2;\n const chartHeight = height - padding * 2;\n\n const allValues = data.datasets.flatMap((d) => d.data);\n const maxValue = Math.max(...allValues) * 1.1;\n const barGroupWidth = chartWidth / data.labels.length;\n const barWidth = barGroupWidth / (data.datasets.length + 1);\n\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">`;\n svg += `<rect width=\"100%\" height=\"100%\" fill=\"white\"/>`;\n\n // Title\n if (options.title) {\n svg += `<text x=\"${width / 2}\" y=\"25\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"bold\">${escapeXml(options.title)}</text>`;\n }\n\n // Y-axis\n svg += `<line x1=\"${padding}\" y1=\"${padding}\" x2=\"${padding}\" y2=\"${height - padding}\" stroke=\"#ccc\"/>`;\n\n // X-axis\n svg += `<line x1=\"${padding}\" y1=\"${height - padding}\" x2=\"${width - padding}\" y2=\"${height - padding}\" stroke=\"#ccc\"/>`;\n\n // Y-axis labels and grid lines\n for (let i = 0; i <= 5; i++) {\n const y = padding + (chartHeight * i) / 5;\n const value = Math.round(maxValue * (1 - i / 5));\n svg += `<line x1=\"${padding}\" y1=\"${y}\" x2=\"${width - padding}\" y2=\"${y}\" stroke=\"#eee\"/>`;\n svg += `<text x=\"${padding - 10}\" y=\"${y + 4}\" text-anchor=\"end\" font-size=\"10\">${value}</text>`;\n }\n\n // Bars\n data.datasets.forEach((dataset, datasetIndex) => {\n const color = dataset.backgroundColor || DEFAULT_COLORS[datasetIndex % DEFAULT_COLORS.length];\n\n dataset.data.forEach((value, index) => {\n const x = padding + index * barGroupWidth + datasetIndex * barWidth + barWidth / 2;\n const barHeight = (value / maxValue) * chartHeight;\n const y = height - padding - barHeight;\n\n svg += `<rect x=\"${x}\" y=\"${y}\" width=\"${barWidth * 0.8}\" height=\"${barHeight}\" fill=\"${color}\"/>`;\n });\n });\n\n // X-axis labels\n data.labels.forEach((label, index) => {\n const x = padding + index * barGroupWidth + barGroupWidth / 2;\n svg += `<text x=\"${x}\" y=\"${height - padding + 20}\" text-anchor=\"middle\" font-size=\"10\">${escapeXml(label)}</text>`;\n });\n\n // Legend\n if (options.legend !== false && data.datasets.length > 1) {\n data.datasets.forEach((dataset, index) => {\n const legendX = width - padding - 100;\n const legendY = padding + index * 20;\n const color = dataset.backgroundColor || DEFAULT_COLORS[index % DEFAULT_COLORS.length];\n svg += `<rect x=\"${legendX}\" y=\"${legendY}\" width=\"12\" height=\"12\" fill=\"${color}\"/>`;\n svg += `<text x=\"${legendX + 16}\" y=\"${legendY + 10}\" font-size=\"10\">${escapeXml(dataset.label)}</text>`;\n });\n }\n\n svg += \"</svg>\";\n return svg;\n}\n\n// Generate SVG line chart\nfunction generateLineChartSVG(data: ChartData, options: ChartOptions): string {\n const width = options.width || 600;\n const height = options.height || 400;\n const padding = 60;\n const chartWidth = width - padding * 2;\n const chartHeight = height - padding * 2;\n\n const allValues = data.datasets.flatMap((d) => d.data);\n const maxValue = Math.max(...allValues) * 1.1;\n const pointSpacing = chartWidth / (data.labels.length - 1 || 1);\n\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">`;\n svg += `<rect width=\"100%\" height=\"100%\" fill=\"white\"/>`;\n\n // Title\n if (options.title) {\n svg += `<text x=\"${width / 2}\" y=\"25\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"bold\">${escapeXml(options.title)}</text>`;\n }\n\n // Axes\n svg += `<line x1=\"${padding}\" y1=\"${padding}\" x2=\"${padding}\" y2=\"${height - padding}\" stroke=\"#ccc\"/>`;\n svg += `<line x1=\"${padding}\" y1=\"${height - padding}\" x2=\"${width - padding}\" y2=\"${height - padding}\" stroke=\"#ccc\"/>`;\n\n // Grid lines\n for (let i = 0; i <= 5; i++) {\n const y = padding + (chartHeight * i) / 5;\n const value = Math.round(maxValue * (1 - i / 5));\n svg += `<line x1=\"${padding}\" y1=\"${y}\" x2=\"${width - padding}\" y2=\"${y}\" stroke=\"#eee\"/>`;\n svg += `<text x=\"${padding - 10}\" y=\"${y + 4}\" text-anchor=\"end\" font-size=\"10\">${value}</text>`;\n }\n\n // Lines\n data.datasets.forEach((dataset, datasetIndex) => {\n const color = dataset.borderColor || DEFAULT_COLORS[datasetIndex % DEFAULT_COLORS.length];\n const points: string[] = [];\n\n dataset.data.forEach((value, index) => {\n const x = padding + index * pointSpacing;\n const y = height - padding - (value / maxValue) * chartHeight;\n points.push(`${x},${y}`);\n });\n\n svg += `<polyline points=\"${points.join(\" \")}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"2\"/>`;\n\n // Data points\n dataset.data.forEach((value, index) => {\n const x = padding + index * pointSpacing;\n const y = height - padding - (value / maxValue) * chartHeight;\n svg += `<circle cx=\"${x}\" cy=\"${y}\" r=\"4\" fill=\"${color}\"/>`;\n });\n });\n\n // X-axis labels\n data.labels.forEach((label, index) => {\n const x = padding + index * pointSpacing;\n svg += `<text x=\"${x}\" y=\"${height - padding + 20}\" text-anchor=\"middle\" font-size=\"10\">${escapeXml(label)}</text>`;\n });\n\n // Legend\n if (options.legend !== false && data.datasets.length > 1) {\n data.datasets.forEach((dataset, index) => {\n const legendX = width - padding - 100;\n const legendY = padding + index * 20;\n const color = dataset.borderColor || DEFAULT_COLORS[index % DEFAULT_COLORS.length];\n svg += `<line x1=\"${legendX}\" y1=\"${legendY + 6}\" x2=\"${legendX + 12}\" y2=\"${legendY + 6}\" stroke=\"${color}\" stroke-width=\"2\"/>`;\n svg += `<text x=\"${legendX + 16}\" y=\"${legendY + 10}\" font-size=\"10\">${escapeXml(dataset.label)}</text>`;\n });\n }\n\n svg += \"</svg>\";\n return svg;\n}\n\n// Generate SVG pie chart\nfunction generatePieChartSVG(data: ChartData, options: ChartOptions): string {\n const width = options.width || 400;\n const height = options.height || 400;\n const centerX = width / 2;\n const centerY = height / 2;\n const radius = Math.min(width, height) / 2 - 60;\n\n const values = data.datasets[0]?.data || [];\n const total = values.reduce((sum, v) => sum + v, 0);\n\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">`;\n svg += `<rect width=\"100%\" height=\"100%\" fill=\"white\"/>`;\n\n // Title\n if (options.title) {\n svg += `<text x=\"${width / 2}\" y=\"25\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"bold\">${escapeXml(options.title)}</text>`;\n }\n\n let startAngle = -Math.PI / 2;\n\n values.forEach((value, index) => {\n const sliceAngle = (value / total) * 2 * Math.PI;\n const endAngle = startAngle + sliceAngle;\n\n const x1 = centerX + radius * Math.cos(startAngle);\n const y1 = centerY + radius * Math.sin(startAngle);\n const x2 = centerX + radius * Math.cos(endAngle);\n const y2 = centerY + radius * Math.sin(endAngle);\n\n const largeArcFlag = sliceAngle > Math.PI ? 1 : 0;\n const colors = data.datasets[0]?.backgroundColor;\n const color = Array.isArray(colors)\n ? colors[index % colors.length]\n : DEFAULT_COLORS[index % DEFAULT_COLORS.length];\n\n svg += `<path d=\"M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z\" fill=\"${color}\"/>`;\n\n // Label\n const labelAngle = startAngle + sliceAngle / 2;\n const labelX = centerX + (radius * 0.7) * Math.cos(labelAngle);\n const labelY = centerY + (radius * 0.7) * Math.sin(labelAngle);\n const percentage = Math.round((value / total) * 100);\n\n svg += `<text x=\"${labelX}\" y=\"${labelY}\" text-anchor=\"middle\" font-size=\"12\" fill=\"white\" font-weight=\"bold\">${percentage}%</text>`;\n\n startAngle = endAngle;\n });\n\n // Legend\n if (options.legend !== false) {\n data.labels.forEach((label, index) => {\n const legendY = height - 40 + (index % 2) * 20;\n const legendX = 20 + Math.floor(index / 2) * 120;\n const colors = data.datasets[0]?.backgroundColor;\n const color = Array.isArray(colors)\n ? colors[index % colors.length]\n : DEFAULT_COLORS[index % DEFAULT_COLORS.length];\n\n svg += `<rect x=\"${legendX}\" y=\"${legendY}\" width=\"12\" height=\"12\" fill=\"${color}\"/>`;\n svg += `<text x=\"${legendX + 16}\" y=\"${legendY + 10}\" font-size=\"10\">${escapeXml(label)}</text>`;\n });\n }\n\n svg += \"</svg>\";\n return svg;\n}\n\n// Escape XML special characters\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n// Generate chart based on type\nexport async function generateChart(\n type: ChartType,\n data: ChartData,\n filename?: string,\n options: ChartOptions = {}\n): Promise<ChartResult> {\n const filePath = filename\n ? isPathAllowed(filename)\n ? filename\n : join(tmpdir(), filename)\n : getTempPath();\n\n try {\n await mkdir(dirname(filePath), { recursive: true });\n\n let svgContent: string;\n\n switch (type) {\n case \"bar\":\n svgContent = generateBarChartSVG(data, options);\n break;\n case \"line\":\n case \"area\":\n svgContent = generateLineChartSVG(data, options);\n break;\n case \"pie\":\n case \"doughnut\":\n svgContent = generatePieChartSVG(data, options);\n break;\n case \"scatter\":\n // Scatter uses line chart without connecting lines\n svgContent = generateLineChartSVG(data, { ...options });\n break;\n default:\n return { success: false, error: `Unsupported chart type: ${type}` };\n }\n\n await writeFile(filePath, svgContent, \"utf-8\");\n\n return {\n success: true,\n filePath,\n svgContent,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n// Quick chart from simple data\nexport async function quickChart(\n type: ChartType,\n labels: string[],\n values: number[],\n title?: string,\n filename?: string\n): Promise<ChartResult> {\n const data: ChartData = {\n labels,\n datasets: [\n {\n label: title || \"Data\",\n data: values,\n backgroundColor: DEFAULT_COLORS,\n },\n ],\n };\n\n return generateChart(type, data, filename, { title });\n}\n\nexport default {\n generateChart,\n quickChart,\n};\n"],"mappings":"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","names":[]}
|