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.
Files changed (51) hide show
  1. package/README.md +156 -0
  2. package/dist/App.js +129 -0
  3. package/dist/components/BoardDetail.js +50 -0
  4. package/dist/components/BoardsList.js +93 -0
  5. package/dist/components/ChatInterface.js +115 -0
  6. package/dist/components/CompactList.js +22 -0
  7. package/dist/components/Dashboard.js +74 -0
  8. package/dist/components/Layout.js +6 -0
  9. package/dist/components/Login.js +62 -0
  10. package/dist/components/MembersList.js +117 -0
  11. package/dist/components/Navigation.js +132 -0
  12. package/dist/components/ProblemDetail.js +576 -0
  13. package/dist/components/ProblemForm.js +126 -0
  14. package/dist/components/ProblemsList.js +118 -0
  15. package/dist/components/QuickActions.js +173 -0
  16. package/dist/components/QuickCreate.js +159 -0
  17. package/dist/components/ReportViewer.js +7 -0
  18. package/dist/components/Settings.js +53 -0
  19. package/dist/components/StatusMonitor.js +47 -0
  20. package/dist/components/layout/Container.js +6 -0
  21. package/dist/components/layout/Grid.js +6 -0
  22. package/dist/components/layout/Header.js +8 -0
  23. package/dist/components/layout/Section.js +6 -0
  24. package/dist/components/layout/Stack.js +7 -0
  25. package/dist/components/ui/Badge.js +16 -0
  26. package/dist/components/ui/Button.js +19 -0
  27. package/dist/components/ui/Card.js +15 -0
  28. package/dist/components/ui/Divider.js +9 -0
  29. package/dist/components/ui/ErrorDisplay.js +10 -0
  30. package/dist/components/ui/Input.js +13 -0
  31. package/dist/components/ui/List.js +13 -0
  32. package/dist/components/ui/Panel.js +11 -0
  33. package/dist/components/ui/Spinner.js +16 -0
  34. package/dist/design/borders.js +51 -0
  35. package/dist/design/colors.js +45 -0
  36. package/dist/design/index.js +30 -0
  37. package/dist/design/spacing.js +41 -0
  38. package/dist/design/typography.js +62 -0
  39. package/dist/index.js +43 -0
  40. package/dist/lib/api.js +204 -0
  41. package/dist/lib/asciiArt.js +56 -0
  42. package/dist/lib/auth.js +55 -0
  43. package/dist/lib/browser.js +68 -0
  44. package/dist/lib/commands.js +53 -0
  45. package/dist/lib/config.js +100 -0
  46. package/dist/lib/scrollIndicators.js +29 -0
  47. package/dist/lib/theme.js +33 -0
  48. package/dist/lib/types.js +1 -0
  49. package/dist/lib/viewport.js +32 -0
  50. package/dist/utils/formatters.js +114 -0
  51. 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
+ }