Initial commit: add .gitignore and README
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
interface AvatarProps {
|
||||
headId: string
|
||||
isActive?: boolean
|
||||
isSpeaking?: boolean
|
||||
summary?: string
|
||||
avatarUrl?: string | null
|
||||
}
|
||||
|
||||
export function Avatar({ headId, isActive, isSpeaking, summary, avatarUrl }: AvatarProps) {
|
||||
const displayName = headId.charAt(0).toUpperCase() + headId.slice(1)
|
||||
return (
|
||||
<div
|
||||
className={`avatar ${isActive ? "active" : ""} ${isSpeaking ? "speaking" : ""}`}
|
||||
data-head={headId}
|
||||
title={summary || displayName}
|
||||
>
|
||||
<div className="avatar-face">
|
||||
{avatarUrl ? (
|
||||
<img src={avatarUrl} alt={displayName} className="avatar-img" />
|
||||
) : (
|
||||
<div className="avatar-placeholder">{headId.slice(0, 2)}</div>
|
||||
)}
|
||||
{isSpeaking && <div className="avatar-mouth" aria-hidden />}
|
||||
</div>
|
||||
<span className="avatar-label">{displayName}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Avatar } from "./Avatar"
|
||||
|
||||
import { AVATAR_URLS } from "../config/avatars"
|
||||
|
||||
interface AvatarGridProps {
|
||||
headIds: string[]
|
||||
activeHeads: string[]
|
||||
speakingHead: string | null
|
||||
headSummaries?: Record<string, string>
|
||||
avatarUrls?: Record<string, string | null>
|
||||
}
|
||||
|
||||
export function AvatarGrid({
|
||||
headIds,
|
||||
activeHeads,
|
||||
speakingHead,
|
||||
headSummaries = {},
|
||||
avatarUrls = AVATAR_URLS,
|
||||
}: AvatarGridProps) {
|
||||
return (
|
||||
<div className="avatar-grid">
|
||||
{headIds.map((id) => (
|
||||
<Avatar
|
||||
key={id}
|
||||
headId={id}
|
||||
isActive={activeHeads.includes(id)}
|
||||
isSpeaking={speakingHead === id}
|
||||
summary={headSummaries[id]}
|
||||
avatarUrl={avatarUrls[id] ?? AVATAR_URLS[id]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FinalResponse } from '../types'
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: { role: 'user' | 'assistant'; content: string; data?: FinalResponse }
|
||||
viewMode: string
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, viewMode }: ChatMessageProps) {
|
||||
const isUser = message.role === 'user'
|
||||
return (
|
||||
<div className={`message ${isUser ? 'user' : 'assistant'}`}>
|
||||
<div className="message-content">{message.content}</div>
|
||||
{!isUser && message.data && (viewMode === 'explain' || viewMode === 'developer') && (
|
||||
<div className="message-meta">
|
||||
<span className="confidence">
|
||||
Confidence: {(message.data.confidence_score * 100).toFixed(0)}%
|
||||
</span>
|
||||
{message.data.head_contributions?.length > 0 && (
|
||||
<span className="heads">
|
||||
Heads: {message.data.head_contributions.map((h) => h.head_id).join(', ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { FinalResponse, HeadContribution } from '../types'
|
||||
|
||||
interface ConsensusPanelProps {
|
||||
response: FinalResponse | null
|
||||
viewMode: string
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
export function ConsensusPanel({ response, viewMode, expanded }: ConsensusPanelProps) {
|
||||
if (!response) return null
|
||||
if (viewMode === 'normal' && !expanded) return null
|
||||
|
||||
const { head_contributions, transparency_report, confidence_score } = response
|
||||
const { agreement_map, safety_report } = transparency_report
|
||||
|
||||
return (
|
||||
<aside className="consensus-panel">
|
||||
<h3>Consensus</h3>
|
||||
<div className="confidence">Confidence: {(confidence_score * 100).toFixed(0)}%</div>
|
||||
<section>
|
||||
<h4>Head contributions</h4>
|
||||
{head_contributions.map((h: HeadContribution) => (
|
||||
<div key={h.head_id} className="head-contribution">
|
||||
<strong>{h.head_id}:</strong> {h.summary}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
{(viewMode === 'explain' || viewMode === 'developer') && (
|
||||
<>
|
||||
<section>
|
||||
<h4>Agreed claims</h4>
|
||||
{agreement_map.agreed_claims.slice(0, 5).map((c, i) => (
|
||||
<div key={i} className="claim">{c.claim_text}</div>
|
||||
))}
|
||||
</section>
|
||||
{agreement_map.disputed_claims.length > 0 && (
|
||||
<section>
|
||||
<h4>Disputed</h4>
|
||||
{agreement_map.disputed_claims.map((c, i) => (
|
||||
<div key={i} className="claim disputed">{c.claim_text}</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{safety_report && (
|
||||
<section>
|
||||
<h4>Safety</h4>
|
||||
<div className="safety-report">{safety_report}</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
interface HeadRingProps {
|
||||
headIds: string[]
|
||||
activeHeads: string[]
|
||||
}
|
||||
|
||||
export function HeadRing({ headIds, activeHeads }: HeadRingProps) {
|
||||
const n = headIds.length
|
||||
const radius = 80
|
||||
|
||||
return (
|
||||
<div className="head-ring">
|
||||
<svg viewBox="-120 -120 240 240" className="head-ring-svg">
|
||||
{headIds.map((id, i) => {
|
||||
const angle = (2 * Math.PI * i) / n - Math.PI / 2
|
||||
const x = radius * Math.cos(angle)
|
||||
const y = radius * Math.sin(angle)
|
||||
const isActive = activeHeads.includes(id)
|
||||
return (
|
||||
<g key={id} transform={`translate(${x}, ${y})`}>
|
||||
<circle r="12" className={`head-glyph ${isActive ? "active" : ""}`} data-head={id} />
|
||||
<text y="4" textAnchor="middle" fontSize="8" fill="currentColor">
|
||||
{id.slice(0, 2)}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user