summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx')
-rw-r--r--makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx536
1 files changed, 0 insertions, 536 deletions
diff --git a/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx b/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx
deleted file mode 100644
index 5caa3c4..0000000
--- a/makima/frontend/src/components/mesh/UnifiedMeshChatInput.tsx
+++ /dev/null
@@ -1,536 +0,0 @@
-import { useState, useCallback, useRef, useEffect } from "react";
-import {
- type LlmModel,
- type UserQuestion,
- type UserAnswer,
- type MeshChatContext,
-} from "../../lib/api";
-import { useMeshChatHistory } from "../../hooks/useMeshChatHistory";
-import { SimpleMarkdown } from "../SimpleMarkdown";
-
-interface UnifiedMeshChatInputProps {
- context: MeshChatContext;
- onUpdate?: () => void;
-}
-
-const MODEL_OPTIONS: { value: LlmModel; label: string }[] = [
- { value: "claude-opus", label: "Claude Opus" },
- { value: "claude-sonnet", label: "Claude Sonnet" },
- { value: "groq", label: "Groq Kimi" },
-];
-
-const DEFAULT_MODEL: LlmModel = "claude-opus";
-
-// LocalStorage keys
-const STORAGE_KEY_MODEL = "makima-mesh-chat-model";
-const STORAGE_KEY_CMD_HISTORY = "makima-mesh-chat-cmd-history";
-const MAX_CMD_HISTORY = 100;
-
-function loadModel(): LlmModel {
- try {
- const modelStr = localStorage.getItem(STORAGE_KEY_MODEL);
- return (modelStr as LlmModel) || DEFAULT_MODEL;
- } catch {
- return DEFAULT_MODEL;
- }
-}
-
-function saveModel(model: LlmModel): void {
- try {
- localStorage.setItem(STORAGE_KEY_MODEL, model);
- } catch {
- // Ignore storage errors
- }
-}
-
-function loadCommandHistory(): string[] {
- try {
- const historyJson = localStorage.getItem(STORAGE_KEY_CMD_HISTORY);
- return historyJson ? JSON.parse(historyJson) : [];
- } catch {
- return [];
- }
-}
-
-function saveCommandHistory(history: string[]): void {
- try {
- localStorage.setItem(
- STORAGE_KEY_CMD_HISTORY,
- JSON.stringify(history.slice(-MAX_CMD_HISTORY))
- );
- } catch {
- // Ignore storage errors
- }
-}
-
-function getPlaceholder(context: MeshChatContext): string {
- switch (context.type) {
- case "mesh":
- return "Create task, list tasks, check status...";
- case "task":
- return "Create subtask, run task, check status...";
- case "subtask":
- return "Update plan, check siblings, merge...";
- default:
- return "Ask anything...";
- }
-}
-
-function getContextLabel(context: MeshChatContext): string {
- switch (context.type) {
- case "mesh":
- return "mesh";
- case "task":
- return `task:${context.taskId?.slice(0, 8)}`;
- case "subtask":
- return `subtask:${context.taskId?.slice(0, 8)}`;
- default:
- return "chat";
- }
-}
-
-export function UnifiedMeshChatInput({
- context,
- onUpdate,
-}: UnifiedMeshChatInputProps) {
- const {
- messages,
- loading: historyLoading,
- error: historyError,
- sending,
- clearHistory,
- sendMessage,
- } = useMeshChatHistory();
-
- const [input, setInput] = useState("");
- const [expanded, setExpanded] = useState(false);
- const [model, setModel] = useState<LlmModel>(DEFAULT_MODEL);
-
- // Pending questions state
- const [pendingQuestions, setPendingQuestions] = useState<
- UserQuestion[] | null
- >(null);
- const [userAnswers, setUserAnswers] = useState<Map<string, string[]>>(
- new Map()
- );
- const [customInputs, setCustomInputs] = useState<Map<string, string>>(
- new Map()
- );
-
- // Command history for arrow key navigation
- const [commandHistory, setCommandHistory] = useState<string[]>([]);
- const [historyIndex, setHistoryIndex] = useState(-1);
- const [savedInput, setSavedInput] = useState("");
-
- const inputRef = useRef<HTMLInputElement>(null);
- const messagesRef = useRef<HTMLDivElement>(null);
-
- // Load model preference on mount
- useEffect(() => {
- setModel(loadModel());
- setCommandHistory(loadCommandHistory());
- }, []);
-
- // Expand when messages exist
- useEffect(() => {
- if (messages.length > 0) {
- setExpanded(true);
- }
- }, [messages.length]);
-
- // Auto-scroll to bottom when messages change
- useEffect(() => {
- if (messagesRef.current) {
- messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
- }
- }, [messages]);
-
- // Handle model change
- const handleModelChange = useCallback((newModel: LlmModel) => {
- setModel(newModel);
- saveModel(newModel);
- }, []);
-
- // Handle keyboard navigation for command history
- const handleKeyDown = useCallback(
- (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "ArrowUp") {
- e.preventDefault();
- if (commandHistory.length === 0) return;
-
- if (historyIndex === -1) {
- setSavedInput(input);
- setHistoryIndex(commandHistory.length - 1);
- setInput(commandHistory[commandHistory.length - 1]);
- } else if (historyIndex > 0) {
- setHistoryIndex(historyIndex - 1);
- setInput(commandHistory[historyIndex - 1]);
- }
- } else if (e.key === "ArrowDown") {
- e.preventDefault();
- if (historyIndex === -1) return;
-
- if (historyIndex < commandHistory.length - 1) {
- setHistoryIndex(historyIndex + 1);
- setInput(commandHistory[historyIndex + 1]);
- } else {
- setHistoryIndex(-1);
- setInput(savedInput);
- }
- }
- },
- [commandHistory, historyIndex, input, savedInput]
- );
-
- const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- if (!input.trim() || sending) return;
-
- const userMessage = input.trim();
-
- // Update command history
- const newHistory =
- commandHistory[commandHistory.length - 1] !== userMessage
- ? [...commandHistory, userMessage]
- : commandHistory;
- setCommandHistory(newHistory);
- saveCommandHistory(newHistory);
-
- // Reset navigation state
- setHistoryIndex(-1);
- setSavedInput("");
-
- setInput("");
- setExpanded(true);
-
- // Send message via hook (uses DB-persisted history)
- const response = await sendMessage(userMessage, context, model);
-
- if (response) {
- // Handle pending questions
- if (response.pendingQuestions?.length) {
- setPendingQuestions(response.pendingQuestions);
- const initialAnswers = new Map<string, string[]>();
- response.pendingQuestions.forEach((q) => {
- initialAnswers.set(q.id, []);
- });
- setUserAnswers(initialAnswers);
- setCustomInputs(new Map());
- }
-
- // Notify parent that something may have been updated
- // Always refresh when tool calls were made (state may have changed)
- if (response.toolCalls && response.toolCalls.length > 0) {
- onUpdate?.();
- }
- }
-
- inputRef.current?.focus();
- },
- [input, sending, context, model, sendMessage, onUpdate, commandHistory]
- );
-
- // Handle option selection for a question
- const handleOptionToggle = useCallback(
- (questionId: string, option: string, allowMultiple: boolean) => {
- setUserAnswers((prev) => {
- const newMap = new Map(prev);
- const currentAnswers = newMap.get(questionId) || [];
-
- if (allowMultiple) {
- if (currentAnswers.includes(option)) {
- newMap.set(
- questionId,
- currentAnswers.filter((a) => a !== option)
- );
- } else {
- newMap.set(questionId, [...currentAnswers, option]);
- }
- } else {
- newMap.set(questionId, [option]);
- }
-
- return newMap;
- });
- },
- []
- );
-
- // Handle custom input change
- const handleCustomInputChange = useCallback(
- (questionId: string, value: string) => {
- setCustomInputs((prev) => {
- const newMap = new Map(prev);
- newMap.set(questionId, value);
- return newMap;
- });
- },
- []
- );
-
- // Submit answers to questions
- const handleSubmitAnswers = useCallback(async () => {
- if (!pendingQuestions || sending) return;
-
- // Build answers array
- const answers: UserAnswer[] = pendingQuestions.map((q) => {
- const selectedOptions = userAnswers.get(q.id) || [];
- const customInput = customInputs.get(q.id)?.trim();
- const finalAnswers = customInput
- ? [...selectedOptions, customInput]
- : selectedOptions;
-
- return {
- id: q.id,
- answers: finalAnswers,
- };
- });
-
- // Format answers as a message
- const answerText = answers
- .map((a) => {
- const question = pendingQuestions.find((q) => q.id === a.id);
- return `${question?.question || a.id}: ${a.answers.join(", ")}`;
- })
- .join("\n");
-
- // Clear pending questions
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
-
- // Send answers as the next message
- const response = await sendMessage(answerText, context, model);
-
- if (response) {
- // Handle more pending questions
- if (response.pendingQuestions?.length) {
- setPendingQuestions(response.pendingQuestions);
- const initialAnswers = new Map<string, string[]>();
- response.pendingQuestions.forEach((q) => {
- initialAnswers.set(q.id, []);
- });
- setUserAnswers(initialAnswers);
- setCustomInputs(new Map());
- }
-
- // Notify parent that something may have been updated
- if (response.toolCalls && response.toolCalls.length > 0) {
- onUpdate?.();
- }
- }
- }, [
- pendingQuestions,
- userAnswers,
- customInputs,
- sending,
- context,
- model,
- sendMessage,
- onUpdate,
- ]);
-
- // Cancel answering questions
- const handleCancelQuestions = useCallback(() => {
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- }, []);
-
- const handleClearHistory = useCallback(async () => {
- await clearHistory();
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- }, [clearHistory]);
-
- const loading = sending || historyLoading;
-
- return (
- <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]">
- {/* Error Display */}
- {historyError && (
- <div className="px-3 py-2 bg-red-900/20 text-red-400 text-xs font-mono">
- {historyError}
- </div>
- )}
-
- {/* Messages Panel (expandable) */}
- {expanded && messages.length > 0 && (
- <div
- ref={messagesRef}
- className="max-h-48 overflow-y-auto p-3 space-y-2 border-b border-[rgba(117,170,252,0.2)]"
- >
- {messages.map((msg) => (
- <div key={msg.id} className="font-mono text-xs">
- {msg.role === "user" && (
- <div className="flex gap-2">
- <span className="text-[#9bc3ff]">&gt;</span>
- <span className="text-white/80 whitespace-pre-wrap">
- {msg.content}
- </span>
- {msg.contextType !== "mesh" && (
- <span className="text-[#555] text-[10px]">
- [{msg.contextType}]
- </span>
- )}
- </div>
- )}
- {msg.role === "assistant" && (
- <div className="pl-4 space-y-1">
- <SimpleMarkdown
- content={msg.content}
- className="text-[#75aafc]"
- />
- {msg.toolCalls && msg.toolCalls.length > 0 && (
- <div className="text-[#555] text-[10px] space-y-0.5">
- {msg.toolCalls.map((tc, i) => (
- <div key={i}>
- <span
- className={
- tc.result.success
- ? "text-green-500"
- : "text-red-400"
- }
- >
- {tc.result.success ? "+" : "x"}
- </span>{" "}
- {tc.name}: {tc.result.message}
- </div>
- ))}
- </div>
- )}
- </div>
- )}
- {msg.role === "error" && (
- <div className="pl-4 text-red-400">{msg.content}</div>
- )}
- </div>
- ))}
- </div>
- )}
-
- {/* Pending Questions UI */}
- {pendingQuestions && pendingQuestions.length > 0 && (
- <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3">
- <div className="text-[#9bc3ff] font-mono text-xs uppercase tracking-wide">
- Questions from AI
- </div>
- {pendingQuestions.map((q) => (
- <div key={q.id} className="space-y-2">
- <div className="text-white/90 font-mono text-sm">
- {q.question}
- </div>
- <div className="flex flex-wrap gap-2">
- {q.options.map((option) => {
- const isSelected = (userAnswers.get(q.id) || []).includes(
- option
- );
- return (
- <button
- key={option}
- type="button"
- onClick={() =>
- handleOptionToggle(q.id, option, q.allowMultiple)
- }
- className={`px-2 py-1 font-mono text-xs border transition-colors ${
- isSelected
- ? "bg-[#3f6fb3] border-[#75aafc] text-white"
- : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-[#3f6fb3]"
- }`}
- >
- {q.allowMultiple && (
- <span className="mr-1">{isSelected ? "+" : "-"}</span>
- )}
- {option}
- </button>
- );
- })}
- </div>
- {q.allowCustom && (
- <input
- type="text"
- value={customInputs.get(q.id) || ""}
- onChange={(e) => handleCustomInputChange(q.id, e.target.value)}
- placeholder="Or type a custom answer..."
- className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]"
- />
- )}
- </div>
- ))}
- <div className="flex gap-2 pt-2">
- <button
- type="button"
- onClick={handleSubmitAnswers}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Submit Answers"}
- </button>
- <button
- type="button"
- onClick={handleCancelQuestions}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
-
- {/* Input Bar */}
- <form onSubmit={handleSubmit} className="flex items-center gap-2 p-3">
- <select
- value={model}
- onChange={(e) => handleModelChange(e.target.value as LlmModel)}
- disabled={loading || !!pendingQuestions}
- className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs px-2 py-1 rounded-none outline-none focus:border-[#3f6fb3] disabled:opacity-50"
- >
- {MODEL_OPTIONS.map((opt) => (
- <option key={opt.value} value={opt.value}>
- {opt.label}
- </option>
- ))}
- </select>
- <span className="text-[#555] font-mono text-[10px]">
- [{getContextLabel(context)}]
- </span>
- <span className="text-[#9bc3ff] font-mono text-sm">&gt;</span>
- <input
- ref={inputRef}
- type="text"
- value={input}
- onChange={(e) => setInput(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder={
- loading
- ? "Processing..."
- : pendingQuestions
- ? "Answer questions above first..."
- : getPlaceholder(context)
- }
- disabled={loading || !!pendingQuestions}
- className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]"
- />
- {messages.length > 0 && (
- <button
- type="button"
- onClick={handleClearHistory}
- className="text-[#555] hover:text-[#9bc3ff] font-mono text-xs transition-colors"
- >
- clear
- </button>
- )}
- <button
- type="submit"
- disabled={loading || !input.trim() || !!pendingQuestions}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Send"}
- </button>
- </form>
- </div>
- );
-}