diff options
| author | soryu <soryu@soryu.co> | 2026-01-16 12:23:49 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-16 12:23:49 +0000 |
| commit | 205ab8a223ddf6591a3e8bfc9108506502977c11 (patch) | |
| tree | d768063acff233dbeea223d7b6ea69d7e3038300 /makima/frontend/src/components/history/ConversationMessage.tsx | |
| parent | 05931d19bc0c161d0177c3f983d0cd903d5e8ae3 (diff) | |
| download | soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.tar.gz soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.zip | |
Fixup: use default api.makima.jp URL and fix default branch detection
Also add checkpointing/history
Diffstat (limited to 'makima/frontend/src/components/history/ConversationMessage.tsx')
| -rw-r--r-- | makima/frontend/src/components/history/ConversationMessage.tsx | 147 |
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> + ); +} |
