summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/history/ConversationMessage.tsx
blob: 43c0ed0c732f5ce4b4a9a955961b81ad2619d6eb (plain) (tree)


















































































































































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