summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/history/ConversationMessage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/history/ConversationMessage.tsx')
-rw-r--r--makima/frontend/src/components/history/ConversationMessage.tsx147
1 files changed, 147 insertions, 0 deletions
diff --git a/makima/frontend/src/components/history/ConversationMessage.tsx b/makima/frontend/src/components/history/ConversationMessage.tsx
new file mode 100644
index 0000000..43c0ed0
--- /dev/null
+++ b/makima/frontend/src/components/history/ConversationMessage.tsx
@@ -0,0 +1,147 @@
+import { useState } from "react";
+import type { ConversationMessage as ConversationMessageType } from "../../lib/api";
+
+interface ConversationMessageProps {
+ message: ConversationMessageType;
+}
+
+// Get role styling
+function getRoleStyle(role: string) {
+ switch (role.toLowerCase()) {
+ case "user":
+ return { label: "User", color: "text-[#9bc3ff]", bg: "bg-[rgba(155,195,255,0.1)]" };
+ case "assistant":
+ return { label: "Assistant", color: "text-emerald-400", bg: "bg-[rgba(52,211,153,0.1)]" };
+ case "system":
+ return { label: "System", color: "text-yellow-400", bg: "bg-[rgba(250,204,21,0.1)]" };
+ case "tool":
+ return { label: "Tool", color: "text-purple-400", bg: "bg-[rgba(192,132,252,0.1)]" };
+ default:
+ return { label: role, color: "text-[#7788aa]", bg: "bg-[rgba(119,136,170,0.1)]" };
+ }
+}
+
+// Format JSON for display
+function formatJson(data: unknown): string {
+ try {
+ return JSON.stringify(data, null, 2);
+ } catch {
+ return String(data);
+ }
+}
+
+export function ConversationMessage({ message }: ConversationMessageProps) {
+ const [showToolDetails, setShowToolDetails] = useState(false);
+ const { label, color, bg } = getRoleStyle(message.role);
+
+ const hasToolInfo = message.toolName || message.toolCalls?.length;
+
+ return (
+ <div className={`p-3 ${bg} border-l-2 border-transparent hover:border-[rgba(117,170,252,0.3)]`}>
+ {/* Header */}
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-2">
+ <span className={`font-mono text-[10px] uppercase ${color}`}>{label}</span>
+ {message.toolName && (
+ <span className="font-mono text-[9px] text-purple-400 px-1.5 py-0.5 border border-[rgba(192,132,252,0.3)]">
+ {message.toolName}
+ </span>
+ )}
+ </div>
+ <div className="flex items-center gap-3">
+ {message.tokenCount && (
+ <span className="font-mono text-[9px] text-[#556677]">
+ {message.tokenCount.toLocaleString()} tokens
+ </span>
+ )}
+ {message.costUsd !== undefined && message.costUsd > 0 && (
+ <span className="font-mono text-[9px] text-[#556677]">
+ ${message.costUsd.toFixed(4)}
+ </span>
+ )}
+ <span className="font-mono text-[9px] text-[#556677]">
+ {new Date(message.timestamp).toLocaleTimeString()}
+ </span>
+ </div>
+ </div>
+
+ {/* Content */}
+ <div className="font-mono text-xs text-[#dbe7ff] whitespace-pre-wrap break-words">
+ {message.content}
+ </div>
+
+ {/* Tool calls */}
+ {hasToolInfo && (
+ <div className="mt-2">
+ <button
+ onClick={() => setShowToolDetails(!showToolDetails)}
+ className="font-mono text-[9px] text-purple-400 hover:text-purple-300 uppercase flex items-center gap-1"
+ >
+ <svg
+ className={`w-3 h-3 transition-transform ${showToolDetails ? "rotate-90" : ""}`}
+ fill="none"
+ stroke="currentColor"
+ viewBox="0 0 24 24"
+ >
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+ </svg>
+ {message.toolCalls?.length
+ ? `${message.toolCalls.length} tool call${message.toolCalls.length > 1 ? "s" : ""}`
+ : "Tool details"}
+ </button>
+
+ {showToolDetails && (
+ <div className="mt-2 space-y-2">
+ {/* Tool input */}
+ {message.toolInput && (
+ <div className="p-2 bg-[rgba(0,0,0,0.3)] border border-[rgba(192,132,252,0.2)]">
+ <div className="font-mono text-[9px] text-purple-400 uppercase mb-1">Input</div>
+ <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto">
+ {formatJson(message.toolInput)}
+ </pre>
+ </div>
+ )}
+
+ {/* Tool result */}
+ {message.toolResult && (
+ <div
+ className={`p-2 border ${
+ message.isError
+ ? "bg-[rgba(239,68,68,0.1)] border-[rgba(239,68,68,0.3)]"
+ : "bg-[rgba(0,0,0,0.3)] border-[rgba(192,132,252,0.2)]"
+ }`}
+ >
+ <div
+ className={`font-mono text-[9px] uppercase mb-1 ${
+ message.isError ? "text-red-400" : "text-purple-400"
+ }`}
+ >
+ {message.isError ? "Error" : "Result"}
+ </div>
+ <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto max-h-48 overflow-y-auto">
+ {message.toolResult}
+ </pre>
+ </div>
+ )}
+
+ {/* Multiple tool calls */}
+ {message.toolCalls?.map((call, i) => (
+ <div
+ key={call.id || i}
+ className="p-2 bg-[rgba(0,0,0,0.3)] border border-[rgba(192,132,252,0.2)]"
+ >
+ <div className="font-mono text-[9px] text-purple-400 uppercase mb-1">
+ {call.name}
+ </div>
+ <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto">
+ {formatJson(call.input)}
+ </pre>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}