Files
grok-cli/src/ui/components/chat-interface.tsx
T

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} />;
}