268 lines
7.9 KiB
TypeScript
268 lines
7.9 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import { Box, Text } from "ink";
|
|
import { GrokAgent, ChatEntry } from "../../agent/grok-agent";
|
|
import { useInputHandler } from "../../hooks/use-input-handler";
|
|
import { LoadingSpinner } from "./loading-spinner";
|
|
import { CommandSuggestions } from "./command-suggestions";
|
|
import { ModelSelection } from "./model-selection";
|
|
import { ChatHistory } from "./chat-history";
|
|
import { ChatInput } from "./chat-input";
|
|
import { MCPStatus } from "./mcp-status";
|
|
import ConfirmationDialog from "./confirmation-dialog";
|
|
import {
|
|
ConfirmationService,
|
|
ConfirmationOptions,
|
|
} from "../../utils/confirmation-service";
|
|
import ApiKeyInput from "./api-key-input";
|
|
import cfonts from "cfonts";
|
|
|
|
interface ChatInterfaceProps {
|
|
agent?: GrokAgent;
|
|
}
|
|
|
|
// Main chat component that handles input when agent is available
|
|
function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) {
|
|
const [chatHistory, setChatHistory] = useState<ChatEntry[]>([]);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [processingTime, setProcessingTime] = useState(0);
|
|
const [tokenCount, setTokenCount] = useState(0);
|
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
const [confirmationOptions, setConfirmationOptions] =
|
|
useState<ConfirmationOptions | null>(null);
|
|
const scrollRef = useRef<any>();
|
|
const processingStartTime = useRef<number>(0);
|
|
|
|
const confirmationService = ConfirmationService.getInstance();
|
|
|
|
const {
|
|
input,
|
|
cursorPosition,
|
|
showCommandSuggestions,
|
|
selectedCommandIndex,
|
|
showModelSelection,
|
|
selectedModelIndex,
|
|
commandSuggestions,
|
|
availableModels,
|
|
autoEditEnabled,
|
|
} = useInputHandler({
|
|
agent,
|
|
chatHistory,
|
|
setChatHistory,
|
|
setIsProcessing,
|
|
setIsStreaming,
|
|
setTokenCount,
|
|
setProcessingTime,
|
|
processingStartTime,
|
|
isProcessing,
|
|
isStreaming,
|
|
isConfirmationActive: !!confirmationOptions,
|
|
});
|
|
|
|
useEffect(() => {
|
|
// Only clear console on non-Windows platforms or if not PowerShell
|
|
// Windows PowerShell can have issues with console.clear() causing flickering
|
|
const isWindows = process.platform === "win32";
|
|
const isPowerShell =
|
|
process.env.ComSpec?.toLowerCase().includes("powershell") ||
|
|
process.env.PSModulePath !== undefined;
|
|
|
|
if (!isWindows || !isPowerShell) {
|
|
console.clear();
|
|
}
|
|
|
|
// Add top padding
|
|
console.log(" ");
|
|
|
|
// Generate logo with margin to match Ink paddingX={2}
|
|
const logoOutput = cfonts.render("GROK", {
|
|
font: "3d",
|
|
align: "left",
|
|
colors: ["magenta", "gray"],
|
|
space: true,
|
|
maxLength: "0",
|
|
gradient: ["magenta", "cyan"],
|
|
independentGradient: false,
|
|
transitionGradient: true,
|
|
env: "node",
|
|
});
|
|
|
|
// Add horizontal margin (2 spaces) to match Ink paddingX={2}
|
|
const logoLines = (logoOutput as any).string.split("\n");
|
|
logoLines.forEach((line: string) => {
|
|
if (line.trim()) {
|
|
console.log(" " + line); // Add 2 spaces for horizontal margin
|
|
} else {
|
|
console.log(line); // Keep empty lines as-is
|
|
}
|
|
});
|
|
|
|
console.log(" "); // Spacing after logo
|
|
|
|
setChatHistory([]);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleConfirmationRequest = (options: ConfirmationOptions) => {
|
|
setConfirmationOptions(options);
|
|
};
|
|
|
|
confirmationService.on("confirmation-requested", handleConfirmationRequest);
|
|
|
|
return () => {
|
|
confirmationService.off(
|
|
"confirmation-requested",
|
|
handleConfirmationRequest
|
|
);
|
|
};
|
|
}, [confirmationService]);
|
|
|
|
useEffect(() => {
|
|
if (!isProcessing && !isStreaming) {
|
|
setProcessingTime(0);
|
|
return;
|
|
}
|
|
|
|
if (processingStartTime.current === 0) {
|
|
processingStartTime.current = Date.now();
|
|
}
|
|
|
|
const interval = setInterval(() => {
|
|
setProcessingTime(
|
|
Math.floor((Date.now() - processingStartTime.current) / 1000)
|
|
);
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [isProcessing, isStreaming]);
|
|
|
|
const handleConfirmation = (dontAskAgain?: boolean) => {
|
|
confirmationService.confirmOperation(true, dontAskAgain);
|
|
setConfirmationOptions(null);
|
|
};
|
|
|
|
const handleRejection = (feedback?: string) => {
|
|
confirmationService.rejectOperation(feedback);
|
|
setConfirmationOptions(null);
|
|
|
|
// Reset processing states when operation is cancelled
|
|
setIsProcessing(false);
|
|
setIsStreaming(false);
|
|
setTokenCount(0);
|
|
setProcessingTime(0);
|
|
processingStartTime.current = 0;
|
|
};
|
|
|
|
return (
|
|
<Box flexDirection="column" paddingX={2}>
|
|
{/* Show tips only when no chat history and no confirmation dialog */}
|
|
{chatHistory.length === 0 && !confirmationOptions && (
|
|
<Box flexDirection="column" marginBottom={2}>
|
|
<Text color="cyan" bold>
|
|
Tips for getting started:
|
|
</Text>
|
|
<Box marginTop={1} flexDirection="column">
|
|
<Text color="gray">
|
|
1. Ask questions, edit files, or run commands.
|
|
</Text>
|
|
<Text color="gray">2. Be specific for the best results.</Text>
|
|
<Text color="gray">
|
|
3. Create GROK.md files to customize your interactions with Grok.
|
|
</Text>
|
|
<Text color="gray">
|
|
4. Press Shift+Tab to toggle auto-edit mode.
|
|
</Text>
|
|
<Text color="gray">5. /help for more information.</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
<Text dimColor>
|
|
Type your request in natural language. Type 'exit' or Ctrl+C to quit.
|
|
</Text>
|
|
</Box>
|
|
|
|
<Box flexDirection="column" ref={scrollRef}>
|
|
<ChatHistory
|
|
entries={chatHistory}
|
|
isConfirmationActive={!!confirmationOptions}
|
|
/>
|
|
</Box>
|
|
|
|
{/* Show confirmation dialog if one is pending */}
|
|
{confirmationOptions && (
|
|
<ConfirmationDialog
|
|
operation={confirmationOptions.operation}
|
|
filename={confirmationOptions.filename}
|
|
showVSCodeOpen={confirmationOptions.showVSCodeOpen}
|
|
content={confirmationOptions.content}
|
|
onConfirm={handleConfirmation}
|
|
onReject={handleRejection}
|
|
/>
|
|
)}
|
|
|
|
{!confirmationOptions && (
|
|
<>
|
|
<LoadingSpinner
|
|
isActive={isProcessing || isStreaming}
|
|
processingTime={processingTime}
|
|
tokenCount={tokenCount}
|
|
/>
|
|
|
|
<ChatInput
|
|
input={input}
|
|
cursorPosition={cursorPosition}
|
|
isProcessing={isProcessing}
|
|
isStreaming={isStreaming}
|
|
/>
|
|
|
|
<Box flexDirection="row">
|
|
<Text color="cyan">
|
|
{autoEditEnabled ? "▶" : "⏸"} auto-edit:{" "}
|
|
{autoEditEnabled ? "on" : "off"}
|
|
</Text>
|
|
<Box marginLeft={1}>
|
|
<Text dimColor>(shift + tab)</Text>
|
|
</Box>
|
|
<Box marginLeft={2}>
|
|
<Text color="yellow">≋ {agent.getCurrentModel()}</Text>
|
|
</Box>
|
|
<MCPStatus />
|
|
</Box>
|
|
|
|
<CommandSuggestions
|
|
suggestions={commandSuggestions}
|
|
input={input}
|
|
selectedIndex={selectedCommandIndex}
|
|
isVisible={showCommandSuggestions}
|
|
/>
|
|
|
|
<ModelSelection
|
|
models={availableModels}
|
|
selectedIndex={selectedModelIndex}
|
|
isVisible={showModelSelection}
|
|
currentModel={agent.getCurrentModel()}
|
|
/>
|
|
</>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Main component that handles API key input or chat interface
|
|
export default function ChatInterface({ agent }: ChatInterfaceProps) {
|
|
const [currentAgent, setCurrentAgent] = useState<GrokAgent | null>(
|
|
agent || null
|
|
);
|
|
|
|
const handleApiKeySet = (newAgent: GrokAgent) => {
|
|
setCurrentAgent(newAgent);
|
|
};
|
|
|
|
if (!currentAgent) {
|
|
return <ApiKeyInput onApiKeySet={handleApiKeySet} />;
|
|
}
|
|
|
|
return <ChatInterfaceWithAgent agent={currentAgent} />;
|
|
}
|