theboardtui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -0
- package/dist/App.js +129 -0
- package/dist/components/BoardDetail.js +50 -0
- package/dist/components/BoardsList.js +93 -0
- package/dist/components/ChatInterface.js +115 -0
- package/dist/components/CompactList.js +22 -0
- package/dist/components/Dashboard.js +74 -0
- package/dist/components/Layout.js +6 -0
- package/dist/components/Login.js +62 -0
- package/dist/components/MembersList.js +117 -0
- package/dist/components/Navigation.js +132 -0
- package/dist/components/ProblemDetail.js +576 -0
- package/dist/components/ProblemForm.js +126 -0
- package/dist/components/ProblemsList.js +118 -0
- package/dist/components/QuickActions.js +173 -0
- package/dist/components/QuickCreate.js +159 -0
- package/dist/components/ReportViewer.js +7 -0
- package/dist/components/Settings.js +53 -0
- package/dist/components/StatusMonitor.js +47 -0
- package/dist/components/layout/Container.js +6 -0
- package/dist/components/layout/Grid.js +6 -0
- package/dist/components/layout/Header.js +8 -0
- package/dist/components/layout/Section.js +6 -0
- package/dist/components/layout/Stack.js +7 -0
- package/dist/components/ui/Badge.js +16 -0
- package/dist/components/ui/Button.js +19 -0
- package/dist/components/ui/Card.js +15 -0
- package/dist/components/ui/Divider.js +9 -0
- package/dist/components/ui/ErrorDisplay.js +10 -0
- package/dist/components/ui/Input.js +13 -0
- package/dist/components/ui/List.js +13 -0
- package/dist/components/ui/Panel.js +11 -0
- package/dist/components/ui/Spinner.js +16 -0
- package/dist/design/borders.js +51 -0
- package/dist/design/colors.js +45 -0
- package/dist/design/index.js +30 -0
- package/dist/design/spacing.js +41 -0
- package/dist/design/typography.js +62 -0
- package/dist/index.js +43 -0
- package/dist/lib/api.js +204 -0
- package/dist/lib/asciiArt.js +56 -0
- package/dist/lib/auth.js +55 -0
- package/dist/lib/browser.js +68 -0
- package/dist/lib/commands.js +53 -0
- package/dist/lib/config.js +100 -0
- package/dist/lib/scrollIndicators.js +29 -0
- package/dist/lib/theme.js +33 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/viewport.js +32 -0
- package/dist/utils/formatters.js +114 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# theboardtui
|
|
2
|
+
|
|
3
|
+
> Terminal UI for The Board - Access your boards and AI features from the command line.
|
|
4
|
+
|
|
5
|
+
A powerful command-line interface that brings The Board's project management and AI capabilities directly to your terminal. Manage boards, run AI analysis, and view reports—all without leaving your terminal.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
Before installing `theboardtui`, ensure you have the following installed on your system:
|
|
10
|
+
|
|
11
|
+
- **Node.js** (version 18 or higher)
|
|
12
|
+
- Download from [nodejs.org](https://nodejs.org/)
|
|
13
|
+
- Verify installation: `node --version`
|
|
14
|
+
- **npm** or **yarn** (comes with Node.js, but yarn is recommended)
|
|
15
|
+
- Verify npm: `npm --version`
|
|
16
|
+
- Install yarn: `npm install -g yarn` (if not already installed)
|
|
17
|
+
- Verify yarn: `yarn --version`
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### Recommended: Using Yarn
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
yarn global add theboardtui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Alternative: Using npm
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g theboardtui
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Verify Installation
|
|
34
|
+
|
|
35
|
+
After installation, verify that `theboardtui` is available:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
theboardtui --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If the command is not found, ensure your global `bin` directory is in your `PATH`. Common locations:
|
|
42
|
+
- macOS/Linux: `~/.yarn/bin` or `~/.npm-global/bin`
|
|
43
|
+
- Windows: `%APPDATA%\npm` or `%LOCALAPPDATA%\Yarn\bin`
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
1. **Run the application:**
|
|
48
|
+
```bash
|
|
49
|
+
theboardtui
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. **First-time setup:**
|
|
53
|
+
- On first run, you'll be prompted to log in with your email and password
|
|
54
|
+
- Your session will be automatically saved for future use
|
|
55
|
+
- No need to log in again unless you explicitly log out
|
|
56
|
+
|
|
57
|
+
3. **Start managing your boards:**
|
|
58
|
+
- Navigate through boards and problems using keyboard shortcuts
|
|
59
|
+
- Run AI analysis on problems
|
|
60
|
+
- View reports and voting results
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- **Board Management** - View and navigate your boards and problems
|
|
65
|
+
- **AI Analysis** - Run AI-powered analysis on problems
|
|
66
|
+
- **AI Chat** - Interactive chat interface with AI assistants
|
|
67
|
+
- **Reports & Voting** - View detailed reports and voting results
|
|
68
|
+
- **Credit Management** - Check your credit balance and usage
|
|
69
|
+
- **Secure Authentication** - Secure session management with automatic token storage
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### Basic Commands
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Start the application
|
|
77
|
+
theboardtui
|
|
78
|
+
|
|
79
|
+
# The application will guide you through:
|
|
80
|
+
# - Login (first time only)
|
|
81
|
+
# - Board selection
|
|
82
|
+
# - Problem management
|
|
83
|
+
# - AI features
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Keyboard Navigation
|
|
87
|
+
|
|
88
|
+
The terminal UI supports standard keyboard navigation:
|
|
89
|
+
- Arrow keys to navigate lists
|
|
90
|
+
- Enter to select items
|
|
91
|
+
- Tab to switch between sections
|
|
92
|
+
- Escape to go back
|
|
93
|
+
- Ctrl+C to exit
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Session data is stored locally in `~/.the-board/` directory. This includes:
|
|
98
|
+
- Authentication tokens
|
|
99
|
+
- User preferences
|
|
100
|
+
- Session state
|
|
101
|
+
|
|
102
|
+
To log out and clear session data, use the logout option in the application settings.
|
|
103
|
+
|
|
104
|
+
## Troubleshooting
|
|
105
|
+
|
|
106
|
+
### Command Not Found
|
|
107
|
+
|
|
108
|
+
If `theboardtui` command is not found after installation:
|
|
109
|
+
|
|
110
|
+
1. **Check your PATH:**
|
|
111
|
+
```bash
|
|
112
|
+
echo $PATH # macOS/Linux
|
|
113
|
+
echo %PATH% # Windows
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
2. **Add yarn global bin to PATH** (if using yarn):
|
|
117
|
+
```bash
|
|
118
|
+
# macOS/Linux - Add to ~/.zshrc or ~/.bashrc
|
|
119
|
+
export PATH="$PATH:$(yarn global bin)"
|
|
120
|
+
|
|
121
|
+
# Then reload your shell
|
|
122
|
+
source ~/.zshrc # or source ~/.bashrc
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
3. **Add npm global bin to PATH** (if using npm):
|
|
126
|
+
```bash
|
|
127
|
+
# macOS/Linux
|
|
128
|
+
export PATH="$PATH:$(npm config get prefix)/bin"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Authentication Issues
|
|
132
|
+
|
|
133
|
+
If you're experiencing login issues:
|
|
134
|
+
- Clear stored session: Delete `~/.the-board/` directory
|
|
135
|
+
- Verify your credentials on [The Board web app](https://theboard11.vercel.app)
|
|
136
|
+
- Ensure you have an active account
|
|
137
|
+
|
|
138
|
+
### Network Issues
|
|
139
|
+
|
|
140
|
+
If the application cannot connect to the API:
|
|
141
|
+
- Check your internet connection
|
|
142
|
+
- Verify the API endpoint is accessible
|
|
143
|
+
- Check for firewall or proxy settings
|
|
144
|
+
|
|
145
|
+
## Support
|
|
146
|
+
|
|
147
|
+
For issues, questions, or feature requests, please visit [The Board](https://theboard11.vercel.app).
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
Copyright (c) Forwardev LLC. All rights reserved.
|
|
152
|
+
|
|
153
|
+
## Terms of Service and Privacy Policy
|
|
154
|
+
|
|
155
|
+
By using this application, you agree to The Board's [Terms of Service](https://theboard11.vercel.app/terms) and [Privacy Policy](https://theboard11.vercel.app/privacy).
|
|
156
|
+
|
package/dist/App.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback } from "react";
|
|
3
|
+
import { Box, useApp, useInput } from "ink";
|
|
4
|
+
import { getSessionToken, getUser, clearSession, setUser } from "./lib/config.js";
|
|
5
|
+
import { apiGet } from "./lib/api.js";
|
|
6
|
+
import Login from "./components/Login.js";
|
|
7
|
+
import QuickActions from "./components/QuickActions.js";
|
|
8
|
+
import Navigation from "./components/Navigation.js";
|
|
9
|
+
import BoardsList from "./components/BoardsList.js";
|
|
10
|
+
import BoardDetail from "./components/BoardDetail.js";
|
|
11
|
+
import ProblemsList from "./components/ProblemsList.js";
|
|
12
|
+
import ProblemDetail from "./components/ProblemDetail.js";
|
|
13
|
+
import QuickCreate from "./components/QuickCreate.js";
|
|
14
|
+
import Settings from "./components/Settings.js";
|
|
15
|
+
import MembersList from "./components/MembersList.js";
|
|
16
|
+
export default function App() {
|
|
17
|
+
const { exit } = useApp();
|
|
18
|
+
const [view, setView] = useState("login");
|
|
19
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
20
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
21
|
+
const [previousView, setPreviousView] = useState(null);
|
|
22
|
+
const [isValidating, setIsValidating] = useState(true);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const validateSession = async () => {
|
|
25
|
+
const token = getSessionToken();
|
|
26
|
+
const storedUser = getUser();
|
|
27
|
+
if (!token || !storedUser.userId || !storedUser.email) {
|
|
28
|
+
setIsValidating(false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const currentUser = await apiGet("/api/auth/me");
|
|
33
|
+
if (currentUser.email !== storedUser.email || currentUser.userId !== storedUser.userId) {
|
|
34
|
+
clearSession();
|
|
35
|
+
setIsValidating(false);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setUser(currentUser.userId, currentUser.email);
|
|
39
|
+
setIsAuthenticated(true);
|
|
40
|
+
setView("quick-actions");
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
clearSession();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setIsValidating(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
validateSession();
|
|
50
|
+
const handleSigInt = () => {
|
|
51
|
+
exit();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
process.on("SIGINT", handleSigInt);
|
|
55
|
+
return () => {
|
|
56
|
+
process.removeListener("SIGINT", handleSigInt);
|
|
57
|
+
};
|
|
58
|
+
}, [exit]);
|
|
59
|
+
const handleLogin = useCallback(() => {
|
|
60
|
+
setIsAuthenticated(true);
|
|
61
|
+
setView("quick-actions");
|
|
62
|
+
}, []);
|
|
63
|
+
const [autoAnalyzeProblemId, setAutoAnalyzeProblemId] = useState(null);
|
|
64
|
+
const handleNavigate = (newView, id, shouldAutoAnalyze) => {
|
|
65
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
66
|
+
if (newView !== "problem-create") {
|
|
67
|
+
setPreviousView(view);
|
|
68
|
+
}
|
|
69
|
+
if (id) {
|
|
70
|
+
setSelectedId(id);
|
|
71
|
+
if (shouldAutoAnalyze) {
|
|
72
|
+
setAutoAnalyzeProblemId(id);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setAutoAnalyzeProblemId(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
setView(newView);
|
|
79
|
+
};
|
|
80
|
+
const handleBack = () => {
|
|
81
|
+
if (process.stdout.isTTY) {
|
|
82
|
+
process.stdout.cursorTo(0, 0);
|
|
83
|
+
process.stdout.clearScreenDown();
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
86
|
+
if (view === "board-detail") {
|
|
87
|
+
setView("boards");
|
|
88
|
+
setSelectedId(null);
|
|
89
|
+
}
|
|
90
|
+
else if (view === "problem-detail") {
|
|
91
|
+
setView("problems");
|
|
92
|
+
setSelectedId(null);
|
|
93
|
+
}
|
|
94
|
+
else if (view === "problem-create") {
|
|
95
|
+
setView(previousView || "quick-actions");
|
|
96
|
+
setSelectedId(null);
|
|
97
|
+
setPreviousView(null);
|
|
98
|
+
}
|
|
99
|
+
else if (view === "members") {
|
|
100
|
+
setView("quick-actions");
|
|
101
|
+
}
|
|
102
|
+
else if (view === "navigation") {
|
|
103
|
+
setView("quick-actions");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
setView("quick-actions");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const handleLogout = () => {
|
|
110
|
+
setIsAuthenticated(false);
|
|
111
|
+
setView("login");
|
|
112
|
+
setSelectedId(null);
|
|
113
|
+
};
|
|
114
|
+
useInput((input, key) => {
|
|
115
|
+
if (key.ctrl && input === "k") {
|
|
116
|
+
setView("navigation");
|
|
117
|
+
}
|
|
118
|
+
else if (key.escape && view === "navigation") {
|
|
119
|
+
setView("quick-actions");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (isValidating) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (!isAuthenticated) {
|
|
126
|
+
return _jsx(Login, { onLogin: handleLogin }, "login");
|
|
127
|
+
}
|
|
128
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [view === "quick-actions" && _jsx(QuickActions, { onNavigate: handleNavigate, onExit: exit }, "quick-actions"), view === "navigation" && _jsx(Navigation, { onNavigate: handleNavigate, onClose: handleBack }, "navigation"), view === "boards" && _jsx(BoardsList, { onNavigate: handleNavigate }, "boards"), view === "board-detail" && selectedId && (_jsx(BoardDetail, { boardId: selectedId, onNavigate: handleNavigate, onBack: handleBack }, `board-${selectedId}`)), view === "problems" && _jsx(ProblemsList, { onNavigate: handleNavigate }, "problems"), view === "problem-detail" && selectedId && (_jsx(ProblemDetail, { problemId: selectedId, onNavigate: handleNavigate, onBack: handleBack, autoAnalyze: autoAnalyzeProblemId === selectedId }, `problem-${selectedId}`)), view === "problem-create" && _jsx(QuickCreate, { onNavigate: handleNavigate, onBack: handleBack }, "problem-create"), view === "members" && _jsx(MembersList, { onNavigate: handleNavigate }, "members"), view === "settings" && _jsx(Settings, { onBack: handleBack, onLogout: handleLogout }, "settings")] }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { apiGet } from "../lib/api.js";
|
|
5
|
+
import { formatDate, truncate } from "../utils/formatters.js";
|
|
6
|
+
import { heading, muted, dim, primary } from "../lib/theme.js";
|
|
7
|
+
import Layout from "./Layout.js";
|
|
8
|
+
import ErrorDisplay from "./ui/ErrorDisplay.js";
|
|
9
|
+
export default function BoardDetail({ boardId, onNavigate, onBack }) {
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [board, setBoard] = useState(null);
|
|
12
|
+
const [members, setMembers] = useState([]);
|
|
13
|
+
const [error, setError] = useState("");
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
loadBoard();
|
|
16
|
+
}, [boardId]);
|
|
17
|
+
const loadBoard = async () => {
|
|
18
|
+
try {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
setError("");
|
|
21
|
+
const [boardData, membersData] = await Promise.all([
|
|
22
|
+
apiGet(`/api/boards/${boardId}`),
|
|
23
|
+
apiGet(`/api/boards/members?boardId=${boardId}`).catch(() => ({ members: [] })),
|
|
24
|
+
]);
|
|
25
|
+
setBoard(boardData.board);
|
|
26
|
+
setMembers(membersData.members || []);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
setError(err.message || "Failed to load board");
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (key.leftArrow || key.escape || (key.ctrl && input === "c")) {
|
|
37
|
+
onBack();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (loading) {
|
|
41
|
+
return (_jsx(Layout, { title: "Board Details", children: _jsx(Text, { children: "Loading..." }) }));
|
|
42
|
+
}
|
|
43
|
+
if (error) {
|
|
44
|
+
return (_jsx(Layout, { title: "Board Details", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4), showHelpText: true, helpText: "Press \u2190/ESC to go back" }) }));
|
|
45
|
+
}
|
|
46
|
+
if (!board) {
|
|
47
|
+
return (_jsxs(Layout, { title: "Board Details", children: [_jsx(Text, { children: "Board not found." }), _jsx(Text, { children: muted("Press ←/ESC to go back") })] }));
|
|
48
|
+
}
|
|
49
|
+
return (_jsx(Layout, { title: truncate(board.name, 40), children: _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { children: [dim("Description:"), " ", board.description ? truncate(board.description, 70) : dim("No description")] }), _jsxs(Text, { children: [dim("Created:"), " ", formatDate(board.createdAt)] }), _jsx(Box, { height: 0 }), _jsx(Text, { children: dim("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") }), _jsx(Text, { bold: true, children: heading(`Members (${members.length})`) }), members.length === 0 ? (_jsx(Text, { children: muted("No members") })) : (members.map((member) => (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsxs(Text, { children: [heading(member.name), " ", dim(`(${member.role})`)] }), member.values.length > 0 && (_jsx(Text, { children: muted(`Values: ${member.values.join(", ")}`) })), member.personality && (_jsx(Text, { children: muted(`Personality: ${member.personality}`) })), _jsx(Text, { children: muted(`Reputation: ${primary(String(member.reputation))}`) })] }, member.id)))), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Press ←/ESC to go back") })] }) }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { apiGet } from "../lib/api.js";
|
|
5
|
+
import { formatDate, formatMemberCount, truncate, wrapWithIndent } from "../utils/formatters.js";
|
|
6
|
+
import { getItemPrefix, getSecondLinePrefix, heading, muted, dim, primary } from "../lib/theme.js";
|
|
7
|
+
import Layout from "./Layout.js";
|
|
8
|
+
import CompactList from "./CompactList.js";
|
|
9
|
+
import ErrorDisplay from "./ui/ErrorDisplay.js";
|
|
10
|
+
export default function BoardsList({ onNavigate }) {
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [boards, setBoards] = useState([]);
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
15
|
+
const [pageSize] = useState(10);
|
|
16
|
+
const [error, setError] = useState("");
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadBoards();
|
|
19
|
+
setSelectedIndex(0);
|
|
20
|
+
}, []);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setSelectedIndex(0);
|
|
23
|
+
}, [currentPage]);
|
|
24
|
+
const loadBoards = async () => {
|
|
25
|
+
try {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError("");
|
|
28
|
+
const data = await apiGet("/api/boards/list");
|
|
29
|
+
setBoards(data.boards || []);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
setError(err.message || "Failed to load boards");
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const totalPages = Math.ceil(boards.length / pageSize);
|
|
39
|
+
const pageStartIndex = (currentPage - 1) * pageSize;
|
|
40
|
+
const pageEndIndex = pageStartIndex + pageSize;
|
|
41
|
+
const pageBoards = boards.slice(pageStartIndex, pageEndIndex);
|
|
42
|
+
useInput((input, key) => {
|
|
43
|
+
if (key.leftArrow || key.escape || (key.ctrl && input === "c")) {
|
|
44
|
+
onNavigate("quick-actions");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (key.pageUp) {
|
|
48
|
+
setCurrentPage((prev) => Math.max(1, prev - 1));
|
|
49
|
+
setSelectedIndex(0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.pageDown) {
|
|
53
|
+
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
|
54
|
+
setSelectedIndex(0);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.upArrow) {
|
|
58
|
+
setSelectedIndex((prev) => {
|
|
59
|
+
if (prev === 0 && currentPage > 1) {
|
|
60
|
+
setCurrentPage((p) => p - 1);
|
|
61
|
+
return pageBoards.length - 1;
|
|
62
|
+
}
|
|
63
|
+
return Math.max(0, prev - 1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (key.downArrow) {
|
|
67
|
+
setSelectedIndex((prev) => {
|
|
68
|
+
const newIndex = Math.min(pageBoards.length - 1, prev + 1);
|
|
69
|
+
if (newIndex === pageBoards.length - 1 && currentPage < totalPages) {
|
|
70
|
+
setCurrentPage((p) => p + 1);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
return newIndex;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (key.return) {
|
|
77
|
+
if (pageBoards[selectedIndex]) {
|
|
78
|
+
const boardIndex = pageStartIndex + selectedIndex;
|
|
79
|
+
onNavigate("board-detail", boards[boardIndex].id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (loading) {
|
|
84
|
+
return (_jsx(Layout, { title: "Boards", children: _jsx(Text, { children: "Loading boards..." }) }));
|
|
85
|
+
}
|
|
86
|
+
if (error) {
|
|
87
|
+
return (_jsx(Layout, { title: "Boards", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4), showHelpText: true, helpText: "Press ESC to go back" }) }));
|
|
88
|
+
}
|
|
89
|
+
if (boards.length === 0) {
|
|
90
|
+
return (_jsxs(Layout, { title: "Boards", children: [_jsx(Text, { children: "No boards found." }), _jsx(Text, { children: muted("Press ESC to go back") })] }));
|
|
91
|
+
}
|
|
92
|
+
return (_jsx(Layout, { title: "Boards", children: _jsx(CompactList, { title: "Boards", items: pageBoards, selectedIndex: selectedIndex, currentPage: currentPage, totalPages: totalPages, totalItems: boards.length, pageSize: pageSize, viewportSize: 6, getItemId: (board) => board.id, renderItem: (board, index, isSelected) => (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsxs(Text, { children: [getItemPrefix(isSelected), heading(board.name), " ", primary(formatMemberCount(board.memberCount))] }), board.description ? (_jsx(_Fragment, { children: wrapWithIndent(truncate(board.description, 70) + " " + dim(`- ${formatDate(board.createdAt)}`), 70, getSecondLinePrefix()).map((line, idx) => (_jsx(Text, { children: line }, idx))) })) : (_jsxs(Text, { children: [getSecondLinePrefix(), dim("No description"), " ", dim(`- ${formatDate(board.createdAt)}`)] }))] })), navigationHint: "\u2191\u2193 navigate | Enter to view | PgUp/PgDn change page | \u2190/ESC back" }) }));
|
|
93
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { apiPost, apiPostStream } from "../lib/api.js";
|
|
5
|
+
import { primary, secondary, muted, dim } from "../lib/theme.js";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import Layout from "./Layout.js";
|
|
8
|
+
export default function ChatInterface({ onBack }) {
|
|
9
|
+
const [messages, setMessages] = useState([]);
|
|
10
|
+
const [input, setInput] = useState("");
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [streamingContent, setStreamingContent] = useState("");
|
|
13
|
+
useInput((inputChar, key) => {
|
|
14
|
+
if (key.escape || (key.ctrl && inputChar === "c")) {
|
|
15
|
+
onBack();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (loading)
|
|
19
|
+
return;
|
|
20
|
+
if (key.return) {
|
|
21
|
+
if (input.trim()) {
|
|
22
|
+
handleSend();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (key.backspace || key.delete) {
|
|
26
|
+
setInput((prev) => prev.slice(0, -1));
|
|
27
|
+
}
|
|
28
|
+
else if (inputChar && !key.ctrl && !key.meta) {
|
|
29
|
+
setInput((prev) => prev + inputChar);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const handleSend = async () => {
|
|
33
|
+
const userMessage = {
|
|
34
|
+
role: "user",
|
|
35
|
+
content: input.trim(),
|
|
36
|
+
timestamp: new Date(),
|
|
37
|
+
};
|
|
38
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
39
|
+
setInput("");
|
|
40
|
+
setLoading(true);
|
|
41
|
+
setStreamingContent("");
|
|
42
|
+
try {
|
|
43
|
+
const newMessages = [
|
|
44
|
+
...messages,
|
|
45
|
+
userMessage,
|
|
46
|
+
{
|
|
47
|
+
role: "assistant",
|
|
48
|
+
content: "",
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
let fullResponse = "";
|
|
53
|
+
try {
|
|
54
|
+
for await (const chunk of apiPostStream("/api/ai/chat", {
|
|
55
|
+
messages: newMessages.slice(0, -1).map((m) => ({
|
|
56
|
+
role: m.role,
|
|
57
|
+
content: m.content,
|
|
58
|
+
})),
|
|
59
|
+
stream: true,
|
|
60
|
+
})) {
|
|
61
|
+
fullResponse += chunk;
|
|
62
|
+
setStreamingContent(fullResponse);
|
|
63
|
+
}
|
|
64
|
+
setMessages((prev) => [
|
|
65
|
+
...prev.slice(0, -1),
|
|
66
|
+
{
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: fullResponse,
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
catch (streamError) {
|
|
74
|
+
const response = await apiPost("/api/ai/chat", {
|
|
75
|
+
messages: newMessages.slice(0, -1).map((m) => ({
|
|
76
|
+
role: m.role,
|
|
77
|
+
content: m.content,
|
|
78
|
+
})),
|
|
79
|
+
stream: false,
|
|
80
|
+
});
|
|
81
|
+
setMessages((prev) => [
|
|
82
|
+
...prev.slice(0, -1),
|
|
83
|
+
{
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: response.content || "",
|
|
86
|
+
timestamp: new Date(),
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
setMessages((prev) => [
|
|
93
|
+
...prev.slice(0, -1),
|
|
94
|
+
{
|
|
95
|
+
role: "assistant",
|
|
96
|
+
content: `Error: ${err.message || "Failed to get response"}`,
|
|
97
|
+
timestamp: new Date(),
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
setStreamingContent("");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const displayMessages = [...messages];
|
|
107
|
+
if (loading && streamingContent) {
|
|
108
|
+
displayMessages.push({
|
|
109
|
+
role: "assistant",
|
|
110
|
+
content: streamingContent,
|
|
111
|
+
timestamp: new Date(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return (_jsx(Layout, { title: "AI Chat", children: _jsxs(Box, { flexDirection: "column", gap: 1, flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, flexGrow: 1, children: [displayMessages.length === 0 ? (_jsx(Text, { children: muted("Start a conversation...") })) : (displayMessages.map((message, index) => (_jsxs(Box, { flexDirection: "column", gap: 0, marginBottom: 1, children: [_jsx(Text, { children: message.role === "user" ? primary("You:") : secondary("AI:") }), _jsx(Text, { children: message.content })] }, index)))), loading && !streamingContent && (_jsx(Text, { children: muted("Thinking...") }))] }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { children: [dim(">"), " ", input, chalk.inverse(" ")] }), _jsx(Text, { children: muted("Type your message and press Enter. ESC to go back.") })] })] }) }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { calculateViewport } from "../lib/viewport.js";
|
|
4
|
+
import { muted, dim } from "../lib/theme.js";
|
|
5
|
+
import { scrollIndicatorTopCompact, scrollIndicatorBottomCompact } from "../lib/scrollIndicators.js";
|
|
6
|
+
export default function CompactList({ title, items, selectedIndex, currentPage, totalPages, totalItems, pageSize, viewportSize = 6, renderItem, getItemId, navigationHint, showPageInfo = true, extraActions, }) {
|
|
7
|
+
const viewport = calculateViewport({
|
|
8
|
+
selectedIndex,
|
|
9
|
+
totalItems: items.length,
|
|
10
|
+
viewportSize,
|
|
11
|
+
keepCentered: true,
|
|
12
|
+
});
|
|
13
|
+
const visibleItems = items.slice(viewport.startIndex, viewport.endIndex);
|
|
14
|
+
const hasMoreAbove = viewport.startIndex > 0;
|
|
15
|
+
const hasMoreBelow = viewport.endIndex < items.length;
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [showPageInfo && (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { children: [muted(`Page ${currentPage} of ${totalPages}`), " ", dim(`(${totalItems} total)`)] }), _jsx(Text, { children: muted("PgUp/PgDn") })] })), _jsx(Box, { height: 1 }), hasMoreAbove && (_jsx(Box, { paddingY: 0, paddingX: 1, children: _jsx(Text, { children: scrollIndicatorTopCompact(true) }) })), visibleItems.map((item, visibleIndex) => {
|
|
17
|
+
const actualIndex = viewport.startIndex + visibleIndex;
|
|
18
|
+
const isSelected = selectedIndex === actualIndex;
|
|
19
|
+
const itemKey = getItemId ? getItemId(item, actualIndex) : String(actualIndex);
|
|
20
|
+
return (_jsx(Box, { paddingY: 0, children: renderItem(item, actualIndex, isSelected) }, itemKey));
|
|
21
|
+
}), hasMoreBelow && (_jsx(Box, { paddingY: 0, paddingX: 1, children: _jsx(Text, { children: scrollIndicatorBottomCompact(true) }) })), _jsx(Box, { height: 1 }), navigationHint && (_jsx(Text, { children: muted(navigationHint) })), extraActions && (_jsx(Box, { paddingY: 0, children: extraActions }))] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { apiGet } from "../lib/api.js";
|
|
5
|
+
import { formatStatus, formatDate, formatCredits } from "../utils/formatters.js";
|
|
6
|
+
import { selectionIndicator, primary, muted, heading, dim } from "../lib/theme.js";
|
|
7
|
+
import Layout from "./Layout.js";
|
|
8
|
+
import ErrorDisplay from "./ui/ErrorDisplay.js";
|
|
9
|
+
export default function Dashboard({ onNavigate }) {
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [stats, setStats] = useState(null);
|
|
12
|
+
const [credits, setCredits] = useState(null);
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
const [error, setError] = useState("");
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
loadData();
|
|
17
|
+
}, []);
|
|
18
|
+
const loadData = async () => {
|
|
19
|
+
try {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
setError("");
|
|
22
|
+
const [statsData, creditsData] = await Promise.all([
|
|
23
|
+
apiGet("/api/dashboard/stats").catch(() => ({ stats: null })),
|
|
24
|
+
apiGet("/api/billing/credits").catch(() => ({ credits: null })),
|
|
25
|
+
]);
|
|
26
|
+
if (statsData.stats) {
|
|
27
|
+
setStats(statsData.stats);
|
|
28
|
+
}
|
|
29
|
+
if (creditsData.credits !== null) {
|
|
30
|
+
setCredits(creditsData.credits);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
setError(err.message || "Failed to load dashboard");
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
useInput((input, key) => {
|
|
41
|
+
if (key.upArrow) {
|
|
42
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
43
|
+
}
|
|
44
|
+
else if (key.downArrow) {
|
|
45
|
+
const maxIndex = 2;
|
|
46
|
+
setSelectedIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
47
|
+
}
|
|
48
|
+
else if (key.return) {
|
|
49
|
+
switch (selectedIndex) {
|
|
50
|
+
case 0:
|
|
51
|
+
onNavigate("boards");
|
|
52
|
+
break;
|
|
53
|
+
case 1:
|
|
54
|
+
onNavigate("problems");
|
|
55
|
+
break;
|
|
56
|
+
case 2:
|
|
57
|
+
onNavigate("settings");
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (loading) {
|
|
63
|
+
return (_jsx(Layout, { title: "Dashboard", children: _jsx(Text, { children: "Loading..." }) }));
|
|
64
|
+
}
|
|
65
|
+
if (error) {
|
|
66
|
+
return (_jsx(Layout, { title: "Dashboard", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4) }) }));
|
|
67
|
+
}
|
|
68
|
+
const menuItems = [
|
|
69
|
+
{ label: "Boards", action: "boards" },
|
|
70
|
+
{ label: "Problems", action: "problems" },
|
|
71
|
+
{ label: "Settings", action: "settings" },
|
|
72
|
+
];
|
|
73
|
+
return (_jsx(Layout, { title: "Dashboard", children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("Quick Stats") }), _jsxs(Text, { children: ["Boards: ", primary(String(stats?.boardCount || 0)), " | Problems: ", primary(String(stats?.problemCount || 0)), " | Credits:", " ", credits !== null ? formatCredits(credits) : muted("N/A")] })] }), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("Navigation") }), menuItems.map((item, index) => (_jsxs(Text, { children: [selectedIndex === index ? selectionIndicator() : " ", item.label] }, item.action)))] }), stats?.recentProblems && stats.recentProblems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("Recent Problems") }), stats.recentProblems.slice(0, 5).map((problem) => (_jsxs(Text, { children: [formatStatus(problem.status), " ", dim(problem.title), " - ", formatDate(problem.createdAt)] }, problem.id)))] })] })), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Use ↑↓ to navigate, Enter to select, ESC to exit") })] }) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { title, divider } from "../lib/theme.js";
|
|
4
|
+
export default function Layout({ children, title: titleText }) {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [titleText && (_jsxs(_Fragment, { children: [_jsx(Box, { paddingX: 1, paddingY: 0, children: _jsx(Text, { bold: true, children: title(titleText) }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { children: divider(50) }) })] })), _jsx(Box, { flexGrow: 1, paddingX: 1, paddingY: titleText ? 1 : 0, children: children })] }));
|
|
6
|
+
}
|