@suman_biswas/beam 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/LICENSE +26 -0
- package/README.md +11 -0
- package/dist/apps/cli/src/Home.js +40 -0
- package/dist/apps/cli/src/Welcome.js +20 -0
- package/dist/apps/cli/src/components/beam.js +17 -0
- package/dist/apps/cli/src/components/spinner.js +27 -0
- package/dist/apps/cli/src/components/thinking.js +23 -0
- package/dist/apps/cli/src/hooks/useHomeState.js +354 -0
- package/dist/apps/cli/src/hooks/useWelcomeState.js +77 -0
- package/dist/apps/cli/src/index.js +77 -0
- package/dist/apps/cli/src/utils/agent.js +156 -0
- package/dist/apps/cli/src/utils/api.js +102 -0
- package/dist/apps/cli/src/utils/constants.js +81 -0
- package/dist/apps/cli/src/utils/tools.js +566 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Beam Non-Commercial License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suman Biswas
|
|
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 use,
|
|
7
|
+
copy, modify, and distribute the Software for personal, educational, internal,
|
|
8
|
+
and other non-commercial purposes, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
1. The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
2. The Software may not be sold, sublicensed for a fee, or used for any
|
|
14
|
+
commercial purpose.
|
|
15
|
+
|
|
16
|
+
3. Commercial purpose includes any use primarily intended for or directed
|
|
17
|
+
toward commercial advantage or monetary compensation, including paid products,
|
|
18
|
+
paid services, consulting deliverables, or resale.
|
|
19
|
+
|
|
20
|
+
4. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { THEME_COLOR, QUICK_ACTIONS } from "./utils/constants.js";
|
|
5
|
+
import { Beam } from "./components/beam.js";
|
|
6
|
+
import { StepSpinner } from "./components/spinner.js";
|
|
7
|
+
import { ThinkingLoader } from "./components/thinking.js";
|
|
8
|
+
import { useHomeState } from "./hooks/useHomeState.js";
|
|
9
|
+
import { MODEL_CONFIGS } from "@beam/shared";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
export const Home = ({ userName, selectedModelId, openAIApiKey, nvidiaApiKey, onModelChange, onApiKeyUpdate, }) => {
|
|
12
|
+
const [currentModelId, setCurrentModelId] = useState(selectedModelId);
|
|
13
|
+
const handleModelChange = (modelId) => {
|
|
14
|
+
setCurrentModelId(modelId);
|
|
15
|
+
onModelChange?.(modelId);
|
|
16
|
+
};
|
|
17
|
+
const { cwd, query, messages, error, isThinking, showMenu, showHelp, showModelSelector, showApiKeySelection, showApiKeyInput, selectedIndex, modelSelectorIndex, apiKeyProviderIndex, tempApiKey, setTempApiKey, tipMessage, columns, modelConfig, handleSubmit, handleOnChange, getStatusColor, providerBadge, API_KEY_PROVIDERS, showFilePicker, filePickerIndex, filteredFiles, filePickerSearch, } = useHomeState({
|
|
18
|
+
selectedModelId: currentModelId,
|
|
19
|
+
openAIApiKey,
|
|
20
|
+
nvidiaApiKey,
|
|
21
|
+
onModelChange: handleModelChange,
|
|
22
|
+
onApiKeyUpdate,
|
|
23
|
+
});
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: THEME_COLOR, children: "\u250C\u2500 " }), _jsxs(Text, { color: THEME_COLOR, bold: true, children: ["Beam", " "] }), _jsx(Text, { dimColor: true, children: "v0.0.0 " }), _jsxs(Text, { color: THEME_COLOR, children: ["─".repeat(Math.max(0, columns - 18)), "\u2510"] })] }), _jsxs(Box, { flexDirection: "row", paddingX: 1, minHeight: 12, children: [_jsx(Box, { flexDirection: "column", width: 1, children: Array(15)
|
|
25
|
+
.fill(0)
|
|
26
|
+
.map((_, i) => (_jsx(Text, { color: THEME_COLOR, children: "\u2502" }, i))) }), _jsxs(Box, { width: "50%", flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["Welcome back ", userName, "!"] }), _jsx(Beam, {}), _jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsxs(Text, { children: [_jsx(Text, { color: THEME_COLOR, children: "Selected Model: " }), _jsx(Text, { color: getStatusColor(), children: "● " }), _jsx(Text, { dimColor: true, children: modelConfig?.tag ?? selectedModelId })] }), _jsx(Text, { dimColor: true, children: cwd })] })] }), _jsx(Box, { flexDirection: "column", width: 1, children: Array(15)
|
|
27
|
+
.fill(0)
|
|
28
|
+
.map((_, i) => (_jsx(Text, { color: THEME_COLOR, dimColor: true, children: "\u2502" }, i))) }), _jsxs(Box, { width: "45%", flexDirection: "column", paddingLeft: 2, paddingY: 1, children: [_jsx(Text, { color: THEME_COLOR, bold: true, children: "Build with Beam" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: tipMessage.lead }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: tipMessage.tip }) }), _jsx(Box, { marginY: 1, children: _jsx(Text, { color: THEME_COLOR, dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), _jsx(Text, { color: THEME_COLOR, bold: true, children: "Quick actions" }), _jsx(Text, { dimColor: true, children: "Type / to explore commands" })] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", width: 1, children: Array(15)
|
|
29
|
+
.fill(0)
|
|
30
|
+
.map((_, i) => (_jsx(Text, { color: THEME_COLOR, children: "\u2502" }, i))) })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: THEME_COLOR, children: ["\u2514", "─".repeat(Math.max(0, columns - 4)), "\u2518"] }) }), _jsx(Box, { marginTop: 1, paddingLeft: 1, children: _jsx(Text, { dimColor: true, children: modelConfig?.description ?? selectedModelId }) })] }), messages.length > 0 && (_jsx(Box, { paddingX: 1, marginTop: 1, marginBottom: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: THEME_COLOR, bold: true, children: ["Beam", " "] }), _jsxs(Text, { dimColor: true, children: ["using ", modelConfig?.tag ?? selectedModelId] })] }) })), messages.length > 0 && (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingBottom: 1, children: messages.map((msg, index) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: msg.role === "user" ? (_jsx(Box, { width: "100%", backgroundColor: "gray", children: _jsxs(Text, { color: "whiteBright", children: ["> ", msg.content] }) })) : msg.role === "plan" ? (_jsx(Box, { paddingLeft: 1, children: msg.isActive ? (_jsx(StepSpinner, { text: msg.content, type: "plan" })) : (_jsxs(Box, { children: [_jsx(Text, { color: THEME_COLOR, dimColor: true, children: "\u25CF" }), _jsxs(Text, { dimColor: true, children: [" ", msg.content] })] })) })) : msg.role === "action" ? (_jsx(Box, { paddingLeft: 1, children: msg.isActive ? (_jsx(StepSpinner, { text: msg.content, type: "action" })) : (_jsxs(Box, { children: [_jsx(Text, { color: THEME_COLOR, dimColor: true, children: "\u25CF" }), _jsxs(Text, { dimColor: true, children: [" ", msg.content] })] })) })) : (_jsx(Text, { children: msg.content })) }, index))) })), isThinking && (_jsx(Box, { paddingLeft: 1, marginBottom: 1, children: _jsx(ThinkingLoader, {}) })), error && (_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "─".repeat(Math.max(0, columns)) }), _jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { bold: true, children: "\u276F " }), !showModelSelector && !showApiKeySelection && !showApiKeyInput ? (_jsx(TextInput, { value: query, placeholder: "", onSubmit: (v) => void (showFilePicker ? null : handleSubmit(v)), onChange: handleOnChange }, `input-${showFilePicker}`)) : (_jsx(Text, { children: query }))] }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(0, columns)) }), _jsx(Box, { paddingLeft: 1, marginBottom: 1, children: showApiKeyInput ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: THEME_COLOR, bold: true, children: ["API Key for ", API_KEY_PROVIDERS[apiKeyProviderIndex]] }) }), _jsx(Text, { dimColor: true, children: "You can paste your API key now or press Enter to skip and add it later." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: THEME_COLOR, bold: true, children: ["\u276F", " "] }), _jsx(TextInput, { value: tempApiKey, onChange: setTempApiKey, placeholder: "Paste API key (optional)..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue \u00B7 ESC to go back" }) })] })) : showApiKeySelection ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: THEME_COLOR, bold: true, children: "Select Provider" }) }), API_KEY_PROVIDERS.map((provider, index) => {
|
|
31
|
+
const isSelected = index === apiKeyProviderIndex;
|
|
32
|
+
return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: isSelected ? "gray" : undefined, children: [_jsx(Text, { color: isSelected ? THEME_COLOR : undefined, children: isSelected ? "❯ " : " " }), _jsx(Text, { color: isSelected ? THEME_COLOR : undefined, bold: isSelected, children: provider })] }) }, provider));
|
|
33
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 to navigate \u00B7 Enter to confirm \u00B7 ESC to cancel" }) })] })) : showModelSelector ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: THEME_COLOR, bold: true, children: "Select Model" }) }), MODEL_CONFIGS.map((model, index) => {
|
|
34
|
+
const isSelected = index === modelSelectorIndex;
|
|
35
|
+
return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: isSelected ? "gray" : undefined, children: [_jsx(Text, { color: isSelected ? THEME_COLOR : undefined, children: isSelected ? "❯ " : " " }), _jsx(Text, { color: isSelected ? THEME_COLOR : undefined, bold: isSelected, children: model.tag }), _jsxs(Text, { dimColor: true, children: [" ", "[", providerBadge(model.provider), "]", isSelected ? ` ${model.description}` : ""] })] }) }, model.id));
|
|
36
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 to navigate \u00B7 Enter to confirm \u00B7 ESC to cancel" }) })] })) : showHelp ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, borderStyle: "round", borderColor: THEME_COLOR, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: THEME_COLOR, bold: true, children: "Beam Help Menu" }) }), _jsx(Text, { children: "Here is some helpful information to get started." }), _jsx(Text, { dimColor: true, children: "To get started, simply ask Beam a question or give it a command." }), _jsx(Text, { dimColor: true, children: "Type /init to create beam configuration. So beam can better understand your project." }), _jsx(Text, { dimColor: true, children: "Type / to get all the supported quick actions you can use with Beam." }), _jsx(Text, { dimColor: true, children: "To add a file as context, type @ and select the file from menu. or type file name." }), _jsx(Text, { dimColor: true, children: "Press ESC to close this dialog." })] })) : showFilePicker ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: THEME_COLOR, bold: true, children: ["Add File", " "] }), _jsx(Text, { dimColor: true, children: filePickerSearch || "type to filter..." })] }), filteredFiles.slice(0, 10).map((file, index) => {
|
|
37
|
+
const isSelected = index === filePickerIndex;
|
|
38
|
+
return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: isSelected ? "gray" : undefined, children: [_jsx(Text, { color: isSelected ? THEME_COLOR : undefined, children: isSelected ? "❯ " : " " }), _jsx(Text, { color: isSelected ? THEME_COLOR : undefined, bold: isSelected, children: file })] }) }, file));
|
|
39
|
+
}), filteredFiles.length === 0 && (_jsxs(Text, { dimColor: true, children: ["No files match \"", filePickerSearch, "\""] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 ESC cancel" }) })] })) : showMenu ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: THEME_COLOR, bold: true, children: "Quick Actions" }) }), QUICK_ACTIONS.map((action, index) => (_jsx(Box, { children: _jsxs(Text, { backgroundColor: index === selectedIndex ? "gray" : undefined, children: [_jsx(Text, { color: THEME_COLOR, children: action.title }), " ", _jsxs(Text, { dimColor: true, children: ["- ", action.description] })] }) }, action.title)))] })) : (_jsx(Text, { dimColor: true, children: "Type ? for help" })) })] })] }));
|
|
40
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { THEME_COLOR } from "./utils/constants.js";
|
|
5
|
+
import { MODEL_CONFIGS } from "@beam/shared";
|
|
6
|
+
import { useWelcomeState } from "./hooks/useWelcomeState.js";
|
|
7
|
+
export const Welcome = ({ onComplete }) => {
|
|
8
|
+
const { columns, step, name, nameError, selectedIndex, apiKey, selectedModel, needsApiKey, frameHeight, setName, setApiKey, handleNameSubmit, handleApiKeySubmit, providerBadge, setNameError, } = useWelcomeState({ onComplete });
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Box, { paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: THEME_COLOR, children: "\u250C\u2500 " }), _jsxs(Text, { color: THEME_COLOR, bold: true, children: ["Beam", " "] }), _jsx(Text, { dimColor: true, children: "v0.0.0 " }), _jsxs(Text, { color: THEME_COLOR, children: ["─".repeat(Math.max(0, columns - 18)), "\u2510"] })] }), _jsxs(Box, { flexDirection: "row", paddingX: 1, minHeight: 12, children: [_jsx(Box, { flexDirection: "column", width: 1, children: Array(frameHeight)
|
|
10
|
+
.fill(0)
|
|
11
|
+
.map((_, i) => (_jsx(Text, { color: THEME_COLOR, children: "\u2502" }, i))) }), _jsxs(Box, { width: columns - 4, flexDirection: "column", paddingX: 3, paddingY: 2, children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", marginBottom: 2, children: [_jsx(Text, { color: THEME_COLOR, bold: true, children: "Welcome to Beam" }), _jsx(Text, { dimColor: true, children: "Let's get you set up before we begin." })] }), _jsxs(Box, { marginBottom: 2, children: [_jsxs(Text, { color: step === "name" ? THEME_COLOR : undefined, children: [step === "name" ? "●" : "✓", " "] }), _jsxs(Text, { color: step === "name" ? THEME_COLOR : undefined, children: ["Your name", " "] }), _jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500 " }), _jsxs(Text, { color: step === "model" ? THEME_COLOR : undefined, children: [step === "model" ? "●" : step === "apiKey" ? "✓" : "○", " "] }), _jsxs(Text, { color: step === "model" ? THEME_COLOR : undefined, children: ["Select model", " "] }), step === "apiKey" && needsApiKey && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500 " }), _jsx(Text, { color: THEME_COLOR, children: "\u25CF " }), _jsx(Text, { color: THEME_COLOR, children: "API key" })] }))] }), step === "name" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "What should we call you?" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: THEME_COLOR, bold: true, children: ["\u276F", " "] }), _jsx(TextInput, { value: name, onChange: (val) => {
|
|
12
|
+
setName(val);
|
|
13
|
+
setNameError("");
|
|
14
|
+
}, onSubmit: handleNameSubmit, placeholder: "Enter your name..." })] }), nameError ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: nameError }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue" }) }))] })), step === "model" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Hi ", _jsx(Text, { color: THEME_COLOR, children: name }), "! Choose your default model:"] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: MODEL_CONFIGS.map((model, i) => {
|
|
15
|
+
const isSelected = i === selectedIndex;
|
|
16
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? THEME_COLOR : undefined, children: isSelected ? "❯ " : " " }), _jsx(Text, { color: isSelected ? THEME_COLOR : undefined, bold: isSelected, children: model.tag }), _jsxs(Text, { dimColor: true, children: [" ", "[", providerBadge(model.provider), "]", isSelected ? ` ${model.description}` : ""] })] }, model.id));
|
|
17
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 to navigate \u00B7 Enter to confirm" }) })] })), step === "apiKey" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Optional API key for", " ", _jsx(Text, { color: THEME_COLOR, children: selectedModel.tag })] }), _jsx(Text, { dimColor: true, children: "You can paste it now or press Enter to skip and add it later." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: THEME_COLOR, bold: true, children: ["\u276F", " "] }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleApiKeySubmit, placeholder: "Paste API key (optional)..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue" }) })] }))] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", width: 1, children: Array(frameHeight)
|
|
18
|
+
.fill(0)
|
|
19
|
+
.map((_, i) => (_jsx(Text, { color: THEME_COLOR, children: "\u2502" }, i))) })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: THEME_COLOR, children: ["\u2514", "─".repeat(Math.max(0, columns - 4)), "\u2518"] }) }), _jsx(Box, { marginTop: 1, paddingLeft: 1, marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "This info is stored locally and never shared." }) })] }));
|
|
20
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
/* prettier-ignore */
|
|
5
|
+
import { Text, Box } from "ink";
|
|
6
|
+
import { THEME_COLOR } from "../utils/constants.js";
|
|
7
|
+
const LOGO = `
|
|
8
|
+
██████╗ ███████╗ █████╗ ███╗ ███╗
|
|
9
|
+
██╔══██╗██╔════╝██╔══██╗████╗ ████║
|
|
10
|
+
██████╔╝█████╗ ███████║██╔████╔██║
|
|
11
|
+
██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║
|
|
12
|
+
██████╔╝███████╗██║ ██║██║ ╚═╝ ██║
|
|
13
|
+
╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
14
|
+
`;
|
|
15
|
+
export function Beam() {
|
|
16
|
+
return (_jsx(Box, { marginY: 1, flexDirection: "column", alignItems: "center", children: _jsx(Text, { color: THEME_COLOR, children: LOGO }) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text, Box, useAnimation } from "ink";
|
|
3
|
+
import { THEME_COLOR } from "../utils/constants.js";
|
|
4
|
+
// Color mapping for different step types
|
|
5
|
+
const STEP_COLORS = {
|
|
6
|
+
plan: THEME_COLOR,
|
|
7
|
+
action: THEME_COLOR,
|
|
8
|
+
};
|
|
9
|
+
export const StepSpinner = ({ text, type }) => {
|
|
10
|
+
const { frame } = useAnimation({ interval: 60 });
|
|
11
|
+
const color = STEP_COLORS[type];
|
|
12
|
+
// Create a pulse wave that moves left to right
|
|
13
|
+
const fullText = `● ${text}`;
|
|
14
|
+
const pulseWidth = 5; // width of the bright area
|
|
15
|
+
const pulsePosition = (frame * 2) % (fullText.length + pulseWidth);
|
|
16
|
+
// Render each character with brightness based on distance from pulse center
|
|
17
|
+
const renderedText = fullText.split("").map((char, index) => {
|
|
18
|
+
const distanceFromPulse = Math.abs(index - pulsePosition);
|
|
19
|
+
const isBright = distanceFromPulse < pulseWidth;
|
|
20
|
+
if (index === 0) {
|
|
21
|
+
// First character is the bullet
|
|
22
|
+
return (_jsx(Text, { color: color, dimColor: !isBright, children: char }, index));
|
|
23
|
+
}
|
|
24
|
+
return (_jsx(Text, { dimColor: !isBright, children: char }, index));
|
|
25
|
+
});
|
|
26
|
+
return _jsx(Box, { children: renderedText });
|
|
27
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useAnimation } from "ink";
|
|
3
|
+
export const ThinkingLoader = () => {
|
|
4
|
+
const { frame } = useAnimation({ interval: 80 });
|
|
5
|
+
// Loading messages that change randomly every 2 seconds
|
|
6
|
+
const loadingMessages = ["Thinking", "Working", "Processing", "Please wait"];
|
|
7
|
+
// Pick a random message every 2 seconds (25 frames at 80ms interval)
|
|
8
|
+
const messageCycle = Math.floor(frame / 25);
|
|
9
|
+
// Use seeded random to ensure same message during the 2-second interval
|
|
10
|
+
const seedHash = (messageCycle * 9973) % loadingMessages.length;
|
|
11
|
+
const messageIndex = seedHash;
|
|
12
|
+
const dotCount = (Math.floor(frame / 3) % 3) + 1; // cycles through 1, 2, 3
|
|
13
|
+
const thinkingText = loadingMessages[messageIndex] + ".".repeat(dotCount);
|
|
14
|
+
const fullThinkingText = thinkingText;
|
|
15
|
+
const pulseWidth = 5;
|
|
16
|
+
const pulsePosition = (frame * 2) % (fullThinkingText.length + pulseWidth);
|
|
17
|
+
const thinkingRendered = fullThinkingText.split("").map((char, index) => {
|
|
18
|
+
const distanceFromPulse = Math.abs(index - pulsePosition);
|
|
19
|
+
const isBright = distanceFromPulse < pulseWidth;
|
|
20
|
+
return (_jsx(Text, { dimColor: !isBright, italic: true, children: char }, index));
|
|
21
|
+
});
|
|
22
|
+
return _jsx(_Fragment, { children: thinkingRendered });
|
|
23
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useInput, useStdout } from "ink";
|
|
3
|
+
import { QUICK_ACTIONS, TIP_MESSAGES } from "../utils/constants.js";
|
|
4
|
+
import { getModelConfig, MODEL_CONFIGS } from "@beam/shared";
|
|
5
|
+
import { runAgent } from "../utils/agent.js";
|
|
6
|
+
import { getProjectAwareness } from "../utils/tools.js";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
// 🎨 UI Development: Set to true to display dummy messages for testing
|
|
11
|
+
const USE_DUMMY_MESSAGES = false;
|
|
12
|
+
const getRandomMessage = () => {
|
|
13
|
+
return TIP_MESSAGES[Math.floor(Math.random() * TIP_MESSAGES.length)];
|
|
14
|
+
};
|
|
15
|
+
export const useHomeState = ({ selectedModelId, openAIApiKey, nvidiaApiKey, onModelChange, onApiKeyUpdate, }) => {
|
|
16
|
+
// Dummy messages for UI development
|
|
17
|
+
const dummyMessages = [
|
|
18
|
+
{ role: "user", content: "What is the current stock price of AAPL?" },
|
|
19
|
+
{ role: "plan", content: "I'll search for the current Apple stock price using webSearch." },
|
|
20
|
+
{ role: "action", content: "webSearch({\"query\":\"AAPL stock price\"}...)", isActive: true },
|
|
21
|
+
{ role: "action", content: "webFetch({\"url\":\"https://finance.yahoo.com...\"}...)" },
|
|
22
|
+
];
|
|
23
|
+
const [query, setQuery] = useState("");
|
|
24
|
+
const [messages, setMessages] = useState(USE_DUMMY_MESSAGES ? dummyMessages : []);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
const [isThinking, setIsThinking] = useState(USE_DUMMY_MESSAGES);
|
|
27
|
+
const [showMenu, setShowMenu] = useState(false);
|
|
28
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
29
|
+
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
30
|
+
const [showApiKeySelection, setShowApiKeySelection] = useState(false);
|
|
31
|
+
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
|
|
32
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
33
|
+
const [modelSelectorIndex, setModelSelectorIndex] = useState(MODEL_CONFIGS.findIndex((m) => m.id === selectedModelId) || 0);
|
|
34
|
+
const [apiKeyProviderIndex, setApiKeyProviderIndex] = useState(0);
|
|
35
|
+
const [tempApiKey, setTempApiKey] = useState("");
|
|
36
|
+
const [tipMessage] = useState(getRandomMessage);
|
|
37
|
+
const [showFilePicker, setShowFilePicker] = useState(false);
|
|
38
|
+
const [filePickerSearch, setFilePickerSearch] = useState("");
|
|
39
|
+
const [filePickerIndex, setFilePickerIndex] = useState(0);
|
|
40
|
+
const [filePickerFiles, setFilePickerFiles] = useState([]);
|
|
41
|
+
const API_KEY_PROVIDERS = ["OpenAI", "NVIDIA"];
|
|
42
|
+
const { stdout } = useStdout();
|
|
43
|
+
const [columns, setColumns] = useState(stdout?.columns || 80);
|
|
44
|
+
const modelConfig = getModelConfig(selectedModelId);
|
|
45
|
+
const filteredFiles = filePickerSearch
|
|
46
|
+
? filePickerFiles.filter(f => f.toLowerCase().includes(filePickerSearch.toLowerCase()))
|
|
47
|
+
: filePickerFiles;
|
|
48
|
+
// 🔹 Input handling
|
|
49
|
+
useInput((input, key) => {
|
|
50
|
+
if (showHelp && key.escape) {
|
|
51
|
+
setShowHelp(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (showApiKeyInput) {
|
|
55
|
+
if (key.return) {
|
|
56
|
+
const provider = API_KEY_PROVIDERS[apiKeyProviderIndex].toLowerCase();
|
|
57
|
+
if (tempApiKey.trim()) {
|
|
58
|
+
onApiKeyUpdate?.(provider, tempApiKey.trim());
|
|
59
|
+
}
|
|
60
|
+
setShowApiKeyInput(false);
|
|
61
|
+
setShowApiKeySelection(false);
|
|
62
|
+
setQuery("");
|
|
63
|
+
setShowMenu(false);
|
|
64
|
+
setTempApiKey("");
|
|
65
|
+
setApiKeyProviderIndex(0);
|
|
66
|
+
}
|
|
67
|
+
else if (key.escape) {
|
|
68
|
+
setShowApiKeyInput(false);
|
|
69
|
+
setShowApiKeySelection(true);
|
|
70
|
+
setTempApiKey("");
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (showApiKeySelection) {
|
|
75
|
+
if (key.upArrow) {
|
|
76
|
+
setApiKeyProviderIndex((i) => Math.max(0, i - 1));
|
|
77
|
+
}
|
|
78
|
+
else if (key.downArrow) {
|
|
79
|
+
setApiKeyProviderIndex((i) => Math.min(API_KEY_PROVIDERS.length - 1, i + 1));
|
|
80
|
+
}
|
|
81
|
+
else if (key.return) {
|
|
82
|
+
setShowApiKeyInput(true);
|
|
83
|
+
}
|
|
84
|
+
else if (key.escape) {
|
|
85
|
+
setShowApiKeySelection(false);
|
|
86
|
+
setQuery("");
|
|
87
|
+
setApiKeyProviderIndex(0);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (showModelSelector) {
|
|
92
|
+
if (key.upArrow) {
|
|
93
|
+
setModelSelectorIndex((i) => Math.max(0, i - 1));
|
|
94
|
+
}
|
|
95
|
+
else if (key.downArrow) {
|
|
96
|
+
setModelSelectorIndex((i) => Math.min(MODEL_CONFIGS.length - 1, i + 1));
|
|
97
|
+
}
|
|
98
|
+
else if (key.return) {
|
|
99
|
+
const selected = MODEL_CONFIGS[modelSelectorIndex];
|
|
100
|
+
onModelChange?.(selected.id);
|
|
101
|
+
setShowModelSelector(false);
|
|
102
|
+
setQuery("");
|
|
103
|
+
setShowMenu(false);
|
|
104
|
+
}
|
|
105
|
+
else if (key.escape) {
|
|
106
|
+
setShowModelSelector(false);
|
|
107
|
+
setQuery("");
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (showFilePicker) {
|
|
112
|
+
const matches = filteredFiles.slice(0, 10);
|
|
113
|
+
if (key.upArrow) {
|
|
114
|
+
setFilePickerIndex(prev => Math.max(0, prev - 1));
|
|
115
|
+
}
|
|
116
|
+
else if (key.downArrow) {
|
|
117
|
+
setFilePickerIndex(prev => Math.min(matches.length - 1, prev + 1));
|
|
118
|
+
}
|
|
119
|
+
else if (key.return) {
|
|
120
|
+
const selected = matches[filePickerIndex];
|
|
121
|
+
if (selected) {
|
|
122
|
+
const atIndex = query.lastIndexOf('@');
|
|
123
|
+
setQuery(query.slice(0, atIndex) + `@${selected} `);
|
|
124
|
+
}
|
|
125
|
+
setShowFilePicker(false);
|
|
126
|
+
setFilePickerSearch("");
|
|
127
|
+
setFilePickerIndex(0);
|
|
128
|
+
}
|
|
129
|
+
else if (key.escape) {
|
|
130
|
+
const atIndex = query.lastIndexOf('@');
|
|
131
|
+
setQuery(query.slice(0, atIndex));
|
|
132
|
+
setShowFilePicker(false);
|
|
133
|
+
setFilePickerSearch("");
|
|
134
|
+
setFilePickerIndex(0);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!showMenu)
|
|
139
|
+
return;
|
|
140
|
+
if (key.downArrow) {
|
|
141
|
+
setSelectedIndex((prev) => prev < QUICK_ACTIONS.length - 1 ? prev + 1 : prev);
|
|
142
|
+
}
|
|
143
|
+
if (key.upArrow) {
|
|
144
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
145
|
+
}
|
|
146
|
+
if (key.return) {
|
|
147
|
+
const selected = QUICK_ACTIONS[selectedIndex];
|
|
148
|
+
if (selected.title === "/model") {
|
|
149
|
+
setShowModelSelector(true);
|
|
150
|
+
setShowMenu(false);
|
|
151
|
+
setQuery("");
|
|
152
|
+
}
|
|
153
|
+
else if (selected.title === "/api-key") {
|
|
154
|
+
setShowApiKeySelection(true);
|
|
155
|
+
setShowMenu(false);
|
|
156
|
+
setQuery("");
|
|
157
|
+
}
|
|
158
|
+
else if (selected.title === "/clear") {
|
|
159
|
+
setMessages([]);
|
|
160
|
+
setError(null);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
handleSubmit(selected.userPrompt, selected.title);
|
|
164
|
+
}
|
|
165
|
+
setShowMenu(false);
|
|
166
|
+
}
|
|
167
|
+
if (key.escape) {
|
|
168
|
+
setShowMenu(false);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// 🔹 Resize
|
|
172
|
+
if (stdout) {
|
|
173
|
+
stdout.on("resize", () => setColumns(stdout.columns));
|
|
174
|
+
}
|
|
175
|
+
async function handleSubmit(value, quickAction) {
|
|
176
|
+
if (!value.trim() || value === "/")
|
|
177
|
+
return;
|
|
178
|
+
if (!modelConfig)
|
|
179
|
+
return;
|
|
180
|
+
// API key guard
|
|
181
|
+
if (modelConfig.needsApiKey) {
|
|
182
|
+
if (modelConfig.provider === "openai" && !openAIApiKey) {
|
|
183
|
+
setError("OpenAI API key not set. Use /api-key to add it.");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (modelConfig.provider === "nvidia" && !nvidiaApiKey) {
|
|
187
|
+
setError("NVIDIA API key not set. Use /api-key to add it.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Process @file mentions
|
|
192
|
+
const atMentionRegex = /@([\w./\-_]+)/g;
|
|
193
|
+
const fileContexts = [];
|
|
194
|
+
let match;
|
|
195
|
+
while ((match = atMentionRegex.exec(value)) !== null) {
|
|
196
|
+
const filePath = match[1];
|
|
197
|
+
try {
|
|
198
|
+
const content = readFileSync(join(cwd, filePath), 'utf-8');
|
|
199
|
+
fileContexts.push(`<file path="${filePath}">\n${content}\n</file>`);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// skip unreadable files
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const processedValue = fileContexts.length > 0
|
|
206
|
+
? `${fileContexts.join('\n\n')}\n\n${value}`
|
|
207
|
+
: value;
|
|
208
|
+
setError(null);
|
|
209
|
+
setQuery("");
|
|
210
|
+
setIsThinking(true);
|
|
211
|
+
// Build chat history including the current message (only user/assistant roles for the API)
|
|
212
|
+
// Only include last 3 messages to avoid hitting token limits, but ensure we include the system prompt if it exists
|
|
213
|
+
const chatHistory = [
|
|
214
|
+
...messages.slice(-3),
|
|
215
|
+
{ role: "user", content: processedValue },
|
|
216
|
+
]
|
|
217
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
218
|
+
.map((m) => ({
|
|
219
|
+
role: m.role,
|
|
220
|
+
content: m.content,
|
|
221
|
+
}));
|
|
222
|
+
// Update messages state after building history
|
|
223
|
+
setMessages((prev) => [...prev, { role: "user", content: value }]);
|
|
224
|
+
try {
|
|
225
|
+
await runAgent({
|
|
226
|
+
model: selectedModelId,
|
|
227
|
+
messages: chatHistory,
|
|
228
|
+
quickAction,
|
|
229
|
+
hasProjectFiles: false,
|
|
230
|
+
cwd,
|
|
231
|
+
opts: { openAIApiKey, nvidiaApiKey },
|
|
232
|
+
onStep: (step) => {
|
|
233
|
+
if (step.type === "plan") {
|
|
234
|
+
setIsThinking(false);
|
|
235
|
+
// Deactivate previous steps and activate this plan
|
|
236
|
+
setMessages((prev) => [
|
|
237
|
+
...prev.map(m => (m.role === "action" || m.role === "plan") && m.isActive
|
|
238
|
+
? { ...m, isActive: false }
|
|
239
|
+
: m),
|
|
240
|
+
{ role: "plan", content: step.text, isActive: true },
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
else if (step.type === "action") {
|
|
244
|
+
// Deactivate previous steps and activate this action
|
|
245
|
+
setMessages((prev) => [
|
|
246
|
+
...prev.map(m => (m.role === "action" || m.role === "plan") && m.isActive
|
|
247
|
+
? { ...m, isActive: false }
|
|
248
|
+
: m),
|
|
249
|
+
{ role: "action", content: step.text, isActive: true },
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
else if (step.type === "output") {
|
|
253
|
+
// Deactivate all previous steps
|
|
254
|
+
setMessages((prev) => prev.map(m => (m.role === "action" || m.role === "plan") && m.isActive
|
|
255
|
+
? { ...m, isActive: false }
|
|
256
|
+
: m));
|
|
257
|
+
setMessages((prev) => [...prev, { role: "assistant", content: step.text }]);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
setIsThinking(false);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function handleOnChange(val) {
|
|
270
|
+
setQuery(val);
|
|
271
|
+
const atIndex = val.lastIndexOf('@');
|
|
272
|
+
if (atIndex !== -1) {
|
|
273
|
+
const search = val.slice(atIndex + 1);
|
|
274
|
+
setFilePickerSearch(search);
|
|
275
|
+
setFilePickerIndex(0);
|
|
276
|
+
// Only open picker if search is empty (just typed @) or doesn't contain spaces yet
|
|
277
|
+
if (!showFilePicker && (search === "" || !search.includes(" "))) {
|
|
278
|
+
setFilePickerFiles(getProjectAwareness(cwd));
|
|
279
|
+
setShowFilePicker(true);
|
|
280
|
+
}
|
|
281
|
+
setShowMenu(false);
|
|
282
|
+
setShowHelp(false);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (showFilePicker) {
|
|
286
|
+
setShowFilePicker(false);
|
|
287
|
+
setFilePickerSearch("");
|
|
288
|
+
}
|
|
289
|
+
if (val === "/") {
|
|
290
|
+
setShowMenu(true);
|
|
291
|
+
setShowHelp(false);
|
|
292
|
+
}
|
|
293
|
+
else if (val === "?") {
|
|
294
|
+
setShowHelp(true);
|
|
295
|
+
setShowMenu(false);
|
|
296
|
+
setQuery("");
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
setShowMenu(false);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function getStatusColor() {
|
|
303
|
+
let color = "green";
|
|
304
|
+
if (modelConfig?.needsApiKey) {
|
|
305
|
+
if (modelConfig.provider === "openai" && !openAIApiKey)
|
|
306
|
+
color = "red";
|
|
307
|
+
if (modelConfig.provider === "nvidia" && !nvidiaApiKey)
|
|
308
|
+
color = "red";
|
|
309
|
+
}
|
|
310
|
+
return color;
|
|
311
|
+
}
|
|
312
|
+
function providerBadge(provider) {
|
|
313
|
+
switch (provider) {
|
|
314
|
+
case "ollama":
|
|
315
|
+
return "local";
|
|
316
|
+
case "openai":
|
|
317
|
+
return "OpenAI";
|
|
318
|
+
case "nvidia":
|
|
319
|
+
return "NVIDIA";
|
|
320
|
+
default:
|
|
321
|
+
return provider;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
cwd,
|
|
326
|
+
query,
|
|
327
|
+
setQuery,
|
|
328
|
+
messages,
|
|
329
|
+
error,
|
|
330
|
+
isThinking,
|
|
331
|
+
showMenu,
|
|
332
|
+
showHelp,
|
|
333
|
+
showModelSelector,
|
|
334
|
+
showApiKeySelection,
|
|
335
|
+
showApiKeyInput,
|
|
336
|
+
selectedIndex,
|
|
337
|
+
modelSelectorIndex,
|
|
338
|
+
apiKeyProviderIndex,
|
|
339
|
+
tempApiKey,
|
|
340
|
+
setTempApiKey,
|
|
341
|
+
tipMessage,
|
|
342
|
+
columns,
|
|
343
|
+
modelConfig,
|
|
344
|
+
handleSubmit,
|
|
345
|
+
handleOnChange,
|
|
346
|
+
getStatusColor,
|
|
347
|
+
providerBadge,
|
|
348
|
+
API_KEY_PROVIDERS,
|
|
349
|
+
showFilePicker,
|
|
350
|
+
filePickerIndex,
|
|
351
|
+
filteredFiles,
|
|
352
|
+
filePickerSearch,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useInput, useStdout } from "ink";
|
|
3
|
+
import { MODEL_CONFIGS } from "@beam/shared";
|
|
4
|
+
export const useWelcomeState = ({ onComplete }) => {
|
|
5
|
+
const { stdout } = useStdout();
|
|
6
|
+
const columns = stdout?.columns || 80;
|
|
7
|
+
const [step, setStep] = useState("name");
|
|
8
|
+
const [name, setName] = useState("");
|
|
9
|
+
const [nameError, setNameError] = useState("");
|
|
10
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
11
|
+
const [apiKey, setApiKey] = useState("");
|
|
12
|
+
const selectedModel = MODEL_CONFIGS[selectedIndex];
|
|
13
|
+
const needsApiKey = selectedModel?.provider !== "ollama";
|
|
14
|
+
// 🔹 Keyboard navigation
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (step !== "model")
|
|
17
|
+
return;
|
|
18
|
+
if (key.upArrow) {
|
|
19
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
20
|
+
}
|
|
21
|
+
else if (key.downArrow) {
|
|
22
|
+
setSelectedIndex((i) => Math.min(MODEL_CONFIGS.length - 1, i + 1));
|
|
23
|
+
}
|
|
24
|
+
else if (key.return) {
|
|
25
|
+
const model = MODEL_CONFIGS[selectedIndex];
|
|
26
|
+
if (model.provider === "ollama") {
|
|
27
|
+
onComplete(name.trim(), model.id);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setApiKey("");
|
|
31
|
+
setStep("apiKey");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
function handleNameSubmit(value) {
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
setNameError("Name cannot be empty.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setNameError("");
|
|
41
|
+
setStep("model");
|
|
42
|
+
}
|
|
43
|
+
function handleApiKeySubmit(value) {
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
onComplete(name.trim(), selectedModel.id, trimmed || undefined);
|
|
46
|
+
}
|
|
47
|
+
function providerBadge(provider) {
|
|
48
|
+
switch (provider) {
|
|
49
|
+
case "ollama":
|
|
50
|
+
return "local";
|
|
51
|
+
case "openai":
|
|
52
|
+
return "OpenAI";
|
|
53
|
+
case "nvidia":
|
|
54
|
+
return "NVIDIA";
|
|
55
|
+
default:
|
|
56
|
+
return provider;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const frameHeight = step === "name" ? 17 : step === "model" ? 20 : 21;
|
|
60
|
+
return {
|
|
61
|
+
columns,
|
|
62
|
+
step,
|
|
63
|
+
name,
|
|
64
|
+
nameError,
|
|
65
|
+
selectedIndex,
|
|
66
|
+
apiKey,
|
|
67
|
+
selectedModel,
|
|
68
|
+
needsApiKey,
|
|
69
|
+
frameHeight,
|
|
70
|
+
setName,
|
|
71
|
+
setApiKey,
|
|
72
|
+
setNameError,
|
|
73
|
+
handleNameSubmit,
|
|
74
|
+
handleApiKeySubmit,
|
|
75
|
+
providerBadge,
|
|
76
|
+
};
|
|
77
|
+
};
|